Skip to main content

Discover packages and classes in a python project.

Project description

Roman Discovery

Micro-framework initialization is a mess

Micro-framework-based projects are clean while they're small. Every micro-framework codebase I've seen suffer from the same problem: a mess in the project initialization module. Sooner or later, your entry point package becomes a soup of ad-hoc environment reads, imports-within-functions, and plug-in initializations.

The infamous create_app() is a boiling broth where architectural rules, dependencies, and common sense don't exist. The core of The Application Factory Pattern, proposed, for example, in the official Flask documentation, and the Flask Mega-Tutorial, legitimize this pattern.

It would be OK to keep that ugly, primordial mess hidden behind a layer of abstraction, but the primitive nature of create_app() leaves no place for the open-closed principle. We need to get back to this module every time we add a new plug-in, a new blueprint, or a new package.

Discovery to the rescue

When it comes to taming the chaos, opinionated structure and automated discovery can help.

  • You describe your application structure, outlining where you keep models, blueprints, controllers, etc.
  • You define auto-discovery rules: what your initialization code does when it finds an object of a specific type.
  • You let roman-discovery do the rest.

It's specifically helpful for frameworks that define resources on the fly with decorators and expect you to import all necessary modules. For example, it can be helpful for Flask to load all your blueprints, initialize extensions, and import SQLAlchemy models.

Visitor pattern is the best name for the approach you like finding patterns in implementation details.

Install

pip install roman-discovery

Glossary

I find it helpful to add some semantic colors to the packages and modules of the app. For this, I introduce the terms "domain package" and "module role."

Domain package -- one of the multiple top-level packages of the application that contains the business logic. Adepts of domain-driven design would define domain packages as containers to encapsulate bounded contexts.

Module Role -- a group of modules or packages (directories with __init__.py files) used for the same purpose. I prefer express roles with module prefixes or second-level packages. For example, files models_users.py and models_groups.py can have the "Models" role and keep your model definitions, and files controllers_users.py and controller_groups.py can have the "Controllers" role and keep the code for your controllers.

Usage with Flask

Using within the opinionated Flask structure was the initial purpose of the package. Use roman_discovery.discover() with roman_discovery.flask.get_flask_rules().

The function expects the following project structure.

myproject

  app.py
  config.py
  services.py

  # Simple flat structure with one module
  # per role in a domain package.
  foo/
    controllers.py
    models.py
    cli.py


  # Flat structure with multiple modules per
  # role in a domain package. Modules of the same
  # role share the same prefix
  bar/
    controllers_api.py
    controllers_admin.py
    models_users.py
    models_projects.py
    cli_users.py
    cli_projects.py

  # Nested structure with one flat package per role
  baz/
    controllers/
      api.py
      admin.py
    models/
      users.py
      projects.py
    cli/
      users.py
      projects.py

With this structure, it will do the following.

  • Scan controllers.py, controllers_*.py and controllers/ to find blueprints and attach the blueprints to the flask application.
  • Import all files in models.py models_*.py and models/ to help flask-migrate find all the SQLAlchemy models to create migrations.
  • Scan cli.py, cli_*.py and cli/ to find flask.cli.AppGroup instances and attach them to Flask's CLI.
  • Scan top-level services.py, find all the instances that have init_app() methods, and call obj.init_app(app=flask_app) for each of them.

An example of your top-level app.py

# file: myproject/app.py
from flask import Flask
from roman_discovery import discover
from roman_discovery.flask import get_flask_rules


def app() -> Flask:
    flask_app = Flask(__name__, instance_relative_config=True)
    flask_app.config.from_object("myproject.config")
    flask_rules = get_flask_rules("myproject", flask_app)
    discover("myproject", flask_rules)
    return flask_app

An example of your top-level services.py

# file: myproject/services.py

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail

db = SQLAlchemy()
migrate = Migrate(db=db)
mail = Mail()

Usage with anything else

You can create your own discovery rules with the discover() function, ModuleRule and ObjectRule. Optionally, you can take advantage of custom matchers, defined in roman_discovery.matchers.

For example, that's how you print all modules and all callable objects within the roman_discovery itself.

from roman_discovery import discover, ModuleRule, ObjectRule

module_printer = ModuleRule(
    name="module printer",
    module_matches=lambda module_name: True,
    module_action=lambda module_name: print(f"Found module {module_name}"),
)

object_printer = ObjectRule(
    name="object printer",
    module_matches=lambda module_name: True,
    object_matches=callable,
    object_action=lambda obj: print(f"Found callable object {obj!r}"),
)

discover("roman_discovery", rules=[module_printer, object_printer])

Why the "roman" prefix?

I use it as my own "pseudo-namespace." If I ever abandon the project, at least the package doesn't occupy a common name.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

roman_discovery-0.3.2.tar.gz (11.7 kB view details)

Uploaded Source

Built Distribution

roman_discovery-0.3.2-py3-none-any.whl (9.0 kB view details)

Uploaded Python 3

File details

Details for the file roman_discovery-0.3.2.tar.gz.

File metadata

  • Download URL: roman_discovery-0.3.2.tar.gz
  • Upload date:
  • Size: 11.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.0.1 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.60.0 CPython/3.9.4

File hashes

Hashes for roman_discovery-0.3.2.tar.gz
Algorithm Hash digest
SHA256 a9e7b33db68afc8089d02c2a2da6642fef33250dda5ee0632bd72556bcb838d4
MD5 5c7da8169cd7d1eb7363f5fac3bac759
BLAKE2b-256 6f020b6dc6fdfa69853021e56309b177bd1224d43f61072ae75a0e6721b7b0fa

See more details on using hashes here.

Provenance

File details

Details for the file roman_discovery-0.3.2-py3-none-any.whl.

File metadata

  • Download URL: roman_discovery-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 9.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.0.1 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.60.0 CPython/3.9.4

File hashes

Hashes for roman_discovery-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 bd54f64203512c4cab1adfe6aea03bef0310280436c0c67eb7d302129a3bcfdd
MD5 2dfa8da8e98ac2c5946f0bda402d2b4a
BLAKE2b-256 2b51e2d1ca2cfd134487ef9dba84fbe7d223ada7330a93194fbd06092d58e389

See more details on using hashes here.

Provenance

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page