From cf6d207e716c1053545143073d9037c5da3df670 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Mon, 8 Apr 2024 13:37:55 -0700 Subject: [PATCH] Refactor CLI commands and add documentation - Change commands to supply domain as an option, with `--domain` - Add documentation for all commands - Tweak mkdocs config for optimum visibility of Level-2 docs --- CHANGELOG.rst | 1 + docs/guides/cli/discovery.md | 40 ++++++++++++ docs/guides/cli/docs.md | 35 +++++++++++ docs/guides/cli/index.md | 22 +++++++ docs/guides/cli/new.md | 62 +++++++++++++++++++ docs/guides/cli/shell.md | 40 ++++++++++++ docs/guides/cli/test.md | 45 ++++++++++++++ docs/guides/index.md | 3 + docs/index.md | 4 +- mkdocs.yml | 26 +++++++- src/protean/cli/__init__.py | 48 ++------------ src/protean/cli/docs.py | 19 +++--- src/protean/cli/generate.py | 39 +++++++----- src/protean/cli/shell.py | 46 ++++++++++++++ .../utils/{domain.py => domain_discovery.py} | 9 ++- tests/cli/test_domain_loading.py | 18 +++++- tests/cli/test_find_domain_by_string.py | 2 +- tests/cli/test_generate_docker_compose.py | 2 +- tests/cli/test_shell.py | 52 +++++++++++++++- tests/domain/test_domain_shell_context.py | 9 +-- tests/domain/test_domain_traversal.py | 14 ++--- tests/support/test_domains/README.md | 4 ++ tests/support/test_domains/test10/domain.py | 7 +++ .../support/test_domains/test11/subdomain.py | 7 +++ tests/support/test_domains/test12/foo12.py | 6 ++ .../test_domains/test6/{post.py => post6.py} | 2 +- .../test6/{publishing.py => publishing6.py} | 0 .../test_domains/test7/{post.py => post7.py} | 2 +- .../test7/{publishing.py => publishing7.py} | 0 .../test_domains/test8/sqlite_domain.py | 4 ++ .../test9/{publishing.py => publishing9.py} | 2 +- tests/utils/support/domain.py | 3 - tests/utils/test_get_version.py | 7 --- tests/utils/test_import_from_full_path.py | 15 ----- tests/utils/test_util.py | 13 ---- 35 files changed, 478 insertions(+), 130 deletions(-) create mode 100644 docs/guides/cli/discovery.md create mode 100644 docs/guides/cli/docs.md create mode 100644 docs/guides/cli/index.md create mode 100644 docs/guides/cli/new.md create mode 100644 docs/guides/cli/shell.md create mode 100644 docs/guides/cli/test.md create mode 100644 docs/guides/index.md create mode 100644 src/protean/cli/shell.py rename src/protean/utils/{domain.py => domain_discovery.py} (94%) create mode 100644 tests/support/test_domains/README.md create mode 100644 tests/support/test_domains/test10/domain.py create mode 100644 tests/support/test_domains/test11/subdomain.py create mode 100644 tests/support/test_domains/test12/foo12.py rename tests/support/test_domains/test6/{post.py => post6.py} (84%) rename tests/support/test_domains/test6/{publishing.py => publishing6.py} (100%) rename tests/support/test_domains/test7/{post.py => post7.py} (84%) rename tests/support/test_domains/test7/{publishing.py => publishing7.py} (100%) rename tests/support/test_domains/test9/{publishing.py => publishing9.py} (85%) delete mode 100644 tests/utils/support/domain.py delete mode 100644 tests/utils/test_get_version.py delete mode 100644 tests/utils/test_import_from_full_path.py delete mode 100644 tests/utils/test_util.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e7d11c6b..651387ee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ DEV --- * Switch from Copier to Typer and add comprehensive tests for project generation +* Switch docs to Material for MkDocs and host on https://docs.proteanhq.com 0.11.0 ------ diff --git a/docs/guides/cli/discovery.md b/docs/guides/cli/discovery.md new file mode 100644 index 00000000..927764c0 --- /dev/null +++ b/docs/guides/cli/discovery.md @@ -0,0 +1,40 @@ +# Domain Discovery + +In most cases, you will use the `protean` script to interact with your domain +over the CLI. The script must be told where to find your domain to load and +initialize it, prepping it for your use. The `--domain` option is used to +specify how to load the domain. + +While `--domain` supports a variety of options for specifying your domain, +the below typical values cover most use cases: + +**(nothing)** + +A "domain" or "subdomain" is imported (as a ".py" file, or package), +automatically detecting a domain (``domain`` or ``subdomain``). + +**`--domain auth`** + +The given name is imported, automatically detecting a domain (``domain`` or +``subdomain``). + +!!! note + + ``--domain`` has three parts: an optional path that sets the current working + directory, a Python file or dotted import path, and an optional variable + name of the instance. The following values demonstrate these + parts: + + - ``--app src/auth`` + Sets the current working directory to ``src`` then imports ``hello``. + + - ``--app auth.domain`` + Imports the path ``auth.domain``. + + - ``--app auth:sso`` + Uses the ``sso`` Protean instance in ``auth``. + + If ``--domain`` is not set, the command will try to import "domain" or + "subdomain" (as a ".py" file, or package) and try to detect an Protean + instance. Within the given import, the command looks for an domain instance + named ``domain`` or ``subdomain``, then any domain instance. \ No newline at end of file diff --git a/docs/guides/cli/docs.md b/docs/guides/cli/docs.md new file mode 100644 index 00000000..5bed3afb --- /dev/null +++ b/docs/guides/cli/docs.md @@ -0,0 +1,35 @@ +# `protean docs` + +The `protean docs preview` command starts a live preview server for Protean +documentation. This allows you to view changes in real-time as you edit. + +## Usage + +```shell +protean docs preview [OPTIONS] +``` + +## Options + +- `--help`: Shows the help message and exits. + +## Running a Preview Server + +To start the live preview server for your project's documentation, simply run +the command without any additional options: + +```shell +protean docs preview` +``` + +This will start a local server, usually accessible via a web browser at a URL +such as `http://localhost:8000`. The exact URL will be displayed in your +command line interface once the server is running: + +```shell +INFO - Building documentation... +INFO - Cleaning site directory +INFO - Documentation built in 0.56 seconds +INFO - [09:45:08] Watching paths for changes: 'docs', 'mkdocs.yml' +INFO - [09:45:08] Serving on http://127.0.0.1:8000/ +``` \ No newline at end of file diff --git a/docs/guides/cli/index.md b/docs/guides/cli/index.md new file mode 100644 index 00000000..07fd7736 --- /dev/null +++ b/docs/guides/cli/index.md @@ -0,0 +1,22 @@ +# Command Line Interface + +When you install Protean, you also get a handy command line interface - the `protean` script - in your virtualenv. Driven by [Typer](https://typer.tiangolo.com), the script gives access to commands that can help you scaffold projects, generate new code, and run background servers. The `--help` option will give more information about any commands and options. + +Most commands accept a domain instance to load and initialize, prepping it for +shell access. The [`--domain`](discovery.md) option is used to specify how to +load the domain. + +| Command | | +| :----------------------------- | :----------------------------------| +| [`protean new`](new.md) | Creating a domain | +| [`protean shell`](shell.md) | Working with the shell | + +!!! note + + **Developing Protean:** There are a few additional commands to help you if you + want to contribute to Protean. + + | Command | | + | :----------------------------- | :----------------------------------| + | [`protean docs`](docs.md) | Documentation helpers | + | [`protean test`](test.md) | Testing helpers | diff --git a/docs/guides/cli/new.md b/docs/guides/cli/new.md new file mode 100644 index 00000000..3697994d --- /dev/null +++ b/docs/guides/cli/new.md @@ -0,0 +1,62 @@ + +# `protean new` + +The `protean new` command initializes a new project with a given name. + +## Usage + +```shell +protean new [OPTIONS] PROJECT_NAME +``` + +## Arguments + +| Argument | Description | Default | Required | +|----------------|-------------------------|---------|----------| +| `PROJECT_NAME` | Name of the new project | None | Yes | + +## Options + +- `--output-dir`, `-o`: Specifies the directory where the project should be +created. If not provided, the current directory is used. + +!!! note + Throws an error if the output directory is not found or not empty. + Combine with `--force` to overwrite existing directory. + +- `--data`, `-d`: Accepts one or more key-value pairs to be included in +the project's configuration. +- `--help`: Shows the help message and exits. + +### Behavior Modifiers + +- `--pretend`, `-p`: Runs the command in a "dry run" mode, showing what +would be done without making any changes. +- `--force`, `-f`: Forces the command to run even if it would overwrite +existing files. + +## Examples + +### Creating a New Project + +To create a new project named "authentication" in the current directory: + +```shell +protean new authentication +``` + +### Specifying an Output Directory + +To create a new project in a specific directory: + +```shell +protean new authentication -o /path/to/directory +``` + +### Using Configuration Data + +To pass key-value pairs for project configuration: + +```shell +protean new authentication -d key1 value1 -d key2 value2 +``` \ No newline at end of file diff --git a/docs/guides/cli/shell.md b/docs/guides/cli/shell.md new file mode 100644 index 00000000..0b2f2ce9 --- /dev/null +++ b/docs/guides/cli/shell.md @@ -0,0 +1,40 @@ +# `protean shell` + +The `shell` command starts an interactive shell with your Protean application +pre-loaded. On the underside, the console command uses `ipython`, so if you've +ever used it, you'll be right at home. This is useful for testing out quick +ideas with code and changing data server-side without using an interface. + +## Usage + +```shell +protean shell [OPTIONS] +``` + +## Options + +| Option | Description | Default | +|-------------|-------------------------------------------|---------| +| `--domain` | Sets the domain context for the shell. | `.` | +| `--help` | Shows the help message and exits. | | + +## Launching the Shell + +To launch the interactive shell with default settings: + +```shell +protean shell +``` + +### Specifying a Domain + +To launch the shell within a specific domain context, use the `--domain` option +followed by the domain name: + +```shell +protean shell --domain auth +``` + +This command will initiate the shell in the context of `auth` domain, allowing +you to perform domain-specific operations more conveniently. Read [Domain +Discovery](discovery.md) for options to specify the domain. \ No newline at end of file diff --git a/docs/guides/cli/test.md b/docs/guides/cli/test.md new file mode 100644 index 00000000..8fbfbeb0 --- /dev/null +++ b/docs/guides/cli/test.md @@ -0,0 +1,45 @@ +# `protean test` + +The `protean test` command is used to run unit tests. You can specify the +category of tests to run using the `--category` option. + +## Usage + +```shell +protean test [OPTIONS] +``` + +## Options + +| Option | Description | Default | +|---------------------|-------------------------------------------|---------| +| `--category`, `-c` | Specifies the category of tests to run. | | +| `--help` | Shows the help message and exits. | | + + +Category Options: + +- `CORE`: Runs core framework tests. This is the default category if none is specified. +- `EVENTSTORE`: Runs tests related to the event store functionalities. +- `DATABASE`: Runs database-related tests. +- `FULL`: Runs all available tests, including core, event store, and database tests. + +## Examples + +### Run core framework tests (the default) + +```shell +protean test +``` + +### Run database related tests + +```shell +protean test --category DATABASE +``` + +### Run all tests with coverage + +```shell +protean test --category FULL +``` diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 00000000..2b277933 --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,3 @@ +# User Guide + +[Command Line Interface](cli/index.md) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index b16a4ed5..e66b0a12 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,10 +19,10 @@ Protean is a DDD and CQRS-based framework that helps you build Event-driven appl Protean helps you build applications that can scale and adapt to growing requirements without significant rework. -At its core, Protean encourages a Domain-Driven Design (DDD) approach to development, with support for artifacts necessary to express your domain succinctly and precisely. It also allows you to remain agnostic to the underlying technology by keeping implementation details out of view. +At its core, Protean encourages a Domain-Driven Design (DDD) approach to development, with out-of-the-box support for tactical patterns necessary to express your domain succinctly and precisely. It also allows you to remain agnostic to the underlying technology by keeping implementation details out of view. Protean can be thought of having three capabilities: - *Service-Oriented* - Develop your application as one or more subdomains that run independently as Microservices - *Event-Driven*: - Use events to propagate changes across subdomains or become eventually consistent within a Bounded Context. -- *Adapter-based*: - Use Remain technology-agnostic by exposing Port interfaces to the infrastructure, with multiple adapters supported out of the box. +- *Adapter-Based*: - Use Remain technology-agnostic by exposing Port interfaces to the infrastructure, with multiple adapters supported out of the box. diff --git a/mkdocs.yml b/mkdocs.yml index ab94cb9a..2d66476e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,14 +22,34 @@ theme: - navigation.tracking - navigation.tabs - navigation.tabs.sticky - - navigation.sections + - navigation.expand - navigation.path - navigation.prune - navigation.indexes - toc.follow - navigation.top + - search.suggest + - search.highlight + - content.tabs.link + - content.tooltips + - content.code.annotate + - content.code.copy + - content.code.select +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences nav: - Protean: index.md + - Guides: + - guides/index.md + - Command Line Interface: + - guides/cli/index.md + - guides/cli/discovery.md + - guides/cli/new.md + - guides/cli/shell.md + - guides/cli/docs.md + - guides/cli/test.md - Community: - - community/index.md - - community/contributing.md \ No newline at end of file + - community/index.md + - community/contributing.md \ No newline at end of file diff --git a/src/protean/cli/__init__.py b/src/protean/cli/__init__.py index 2bd01abe..d4602b08 100644 --- a/src/protean/cli/__init__.py +++ b/src/protean/cli/__init__.py @@ -16,29 +16,28 @@ """ import subprocess -import sys -import typing from enum import Enum from typing import Optional import typer -from IPython.terminal.embed import InteractiveShellEmbed from rich import print from typing_extensions import Annotated from protean.cli.docs import app as docs_app from protean.cli.generate import app as generate_app from protean.cli.new import new +from protean.cli.shell import shell from protean.exceptions import NoDomainException -from protean.utils.domain import derive_domain +from protean.utils.domain_discovery import derive_domain # Create the Typer app # `no_args_is_help=True` will show the help message when no arguments are passed app = typer.Typer(no_args_is_help=True) app.command()(new) +app.command()(shell) app.add_typer(generate_app, name="generate") app.add_typer(docs_app, name="docs") @@ -126,14 +125,14 @@ def test( @app.command() def server( - domain_path: Annotated[str, typer.Argument()] = "", + domain: Annotated[str, typer.Option("--domain")] = ".", test_mode: Annotated[Optional[bool], typer.Option()] = False, ): """Run Async Background Server""" # FIXME Accept MAX_WORKERS as command-line input as well from protean.server import Engine - domain = derive_domain(domain_path) + domain = derive_domain(domain) if not domain: raise NoDomainException( "Could not locate a Protean domain. You should provide a domain in" @@ -143,40 +142,3 @@ def server( engine = Engine(domain, test_mode=test_mode) engine.run() - - -@app.command() -def shell(domain_path: Annotated[str, typer.Argument()] = ""): - """Run an interactive Python shell in the context of a given - Protean domain. The domain will populate the default - namespace of this shell according to its configuration. - - This is useful for executing small snippets of code - without having to manually configure the application. - - FIXME: Populate context in a decorator like Flask does: - https://github.com/pallets/flask/blob/b90a4f1f4a370e92054b9cc9db0efcb864f87ebe/src/flask/cli.py#L368 - https://github.com/pallets/flask/blob/b90a4f1f4a370e92054b9cc9db0efcb864f87ebe/src/flask/cli.py#L984 - """ - domain = derive_domain(domain_path) - if not domain: - raise NoDomainException( - "Could not locate a Protean domain. You should provide a domain in" - '"PROTEAN_DOMAIN" environment variable or pass a domain file in options ' - 'and a "domain.py" module was not found in the current directory.' - ) - - with domain.domain_context(): - domain.init() - - ctx: dict[str, typing.Any] = {} - ctx.update(domain.make_shell_context()) - - banner = ( - f"Python {sys.version} on {sys.platform}\n" - f" location: {sys.executable}\n" - f"Domain: {domain.domain_name}\n" - ) - ipshell = InteractiveShellEmbed(banner1=banner, user_ns=ctx) - - ipshell() diff --git a/src/protean/cli/docs.py b/src/protean/cli/docs.py index bf9690d1..d9300f9e 100644 --- a/src/protean/cli/docs.py +++ b/src/protean/cli/docs.py @@ -5,16 +5,19 @@ app = typer.Typer(no_args_is_help=True) -@app.callback() -def callback(): - """ - If we want to create a CLI app with one single command but - still want it to be a command/subcommand, we need to add a callback. +""" +If we want to create a CLI app with one single command but +still want it to be a command/subcommand, we need to add a callback (see below). + +This can be removed when we have more than one command/subcommand. - This can be removed when we have more than one command/subcommand. +https://typer.tiangolo.com/tutorial/commands/one-or-multiple/#one-command-and-one-callback +""" - https://typer.tiangolo.com/tutorial/commands/one-or-multiple/#one-command-and-one-callback - """ + +@app.callback() +def callback(): + pass @app.command() diff --git a/src/protean/cli/generate.py b/src/protean/cli/generate.py index 657f801d..33881a93 100644 --- a/src/protean/cli/generate.py +++ b/src/protean/cli/generate.py @@ -2,32 +2,41 @@ from typing_extensions import Annotated -from protean.utils.domain import derive_domain +from protean.exceptions import NoDomainException +from protean.utils.domain_discovery import derive_domain app = typer.Typer(no_args_is_help=True) -@app.callback() -def callback(): - """ - If we want to create a CLI app with one single command but - still want it to be a command/subcommand, we need to add a callback. +""" +If we want to create a CLI app with one single command but +still want it to be a command/subcommand, we need to add a callback (see below). - This can be removed when we have more than one command/subcommand. +This can be removed when we have more than one command/subcommand. - https://typer.tiangolo.com/tutorial/commands/one-or-multiple/#one-command-and-one-callback - """ +https://typer.tiangolo.com/tutorial/commands/one-or-multiple/#one-command-and-one-callback +""" + + +@app.callback() +def callback(): + pass @app.command() def docker_compose( - domain_path: Annotated[str, typer.Argument()], + domain: Annotated[str, typer.Option()] = ".", ): """Generate a `docker-compose.yml` from Domain config""" - print(f"Generating docker-compose.yml for domain at {domain_path}") - domain = derive_domain(domain_path) - - with domain.domain_context(): - domain.init() + print(f"Generating docker-compose.yml for domain at {domain}") + domain_instance = derive_domain(domain) + if not domain_instance: + raise NoDomainException( + "Could not locate a Protean domain. You should provide a domain in" + '"PROTEAN_DOMAIN" environment variable or pass a domain file in options' + ) + + with domain_instance.domain_context(): + domain_instance.init() # FIXME Generate docker-compose.yml from domain config diff --git a/src/protean/cli/shell.py b/src/protean/cli/shell.py new file mode 100644 index 00000000..2d34fadf --- /dev/null +++ b/src/protean/cli/shell.py @@ -0,0 +1,46 @@ +"""Run an interactive Python shell in the context of a given + Protean domain. The domain will populate the default + namespace of this shell according to its configuration. + + This is useful for executing small snippets of code + without having to manually configure the application. + + FIXME: Populate context in a decorator like Flask does: + https://github.com/pallets/flask/blob/b90a4f1f4a370e92054b9cc9db0efcb864f87ebe/src/flask/cli.py#L368 + https://github.com/pallets/flask/blob/b90a4f1f4a370e92054b9cc9db0efcb864f87ebe/src/flask/cli.py#L984 + """ + +import sys +import typing + +import typer + +from IPython.terminal.embed import InteractiveShellEmbed +from typing_extensions import Annotated + +from protean.exceptions import NoDomainException +from protean.utils.domain_discovery import derive_domain + + +def shell(domain: Annotated[str, typer.Option()] = "."): + domain_instance = derive_domain(domain) + if not domain_instance: + raise NoDomainException( + "Could not locate a Protean domain. You should provide a domain in" + '"PROTEAN_DOMAIN" environment variable or pass a domain file in options' + ) + + with domain_instance.domain_context(): + domain_instance.init() + + ctx: dict[str, typing.Any] = {} + ctx.update(domain_instance.make_shell_context()) + + banner = ( + f"Python {sys.version} on {sys.platform}\n" + f" location: {sys.executable}\n" + f"Domain: {domain_instance.domain_name}\n" + ) + ipshell = InteractiveShellEmbed(banner1=banner, user_ns=ctx) + + ipshell() diff --git a/src/protean/utils/domain.py b/src/protean/utils/domain_discovery.py similarity index 94% rename from src/protean/utils/domain.py rename to src/protean/utils/domain_discovery.py index 6955f2fd..c1adde02 100644 --- a/src/protean/utils/domain.py +++ b/src/protean/utils/domain_discovery.py @@ -127,6 +127,13 @@ def prepare_import(path): """ path = os.path.realpath(path) + # If the path is ".", look for domain.py or subdomain.py in the current directory + if path == os.path.realpath("."): + if os.path.exists(os.path.join(path, "domain.py")): + path = os.path.join(path, "domain.py") + elif os.path.exists(os.path.join(path, "subdomain.py")): + path = os.path.join(path, "subdomain.py") + filename, ext = os.path.splitext(path) if ext == ".py": path = filename @@ -176,7 +183,7 @@ def locate_domain(module_name, domain_name, raise_if_not_found=True): return find_domain_by_string(module, domain_name) -def derive_domain(domain_path): +def derive_domain(domain_path: str = None): """Derive domain from supplied domain path. Domain is derived from sources in this order: diff --git a/tests/cli/test_domain_loading.py b/tests/cli/test_domain_loading.py index e2234e56..cdf8ce2e 100644 --- a/tests/cli/test_domain_loading.py +++ b/tests/cli/test_domain_loading.py @@ -9,7 +9,7 @@ from protean import Domain from protean.cli import NoDomainException -from protean.utils.domain import derive_domain, find_domain_in_module +from protean.utils.domain_discovery import derive_domain, find_domain_in_module from tests.shared import change_working_directory_to @@ -48,7 +48,7 @@ class Module: class TestDomainLoading: @pytest.fixture(autouse=True) - def reset_path(self, request): + def reset_path(self): """Reset sys.path after every test run""" original_path = sys.path[:] cwd = Path.cwd() @@ -91,3 +91,17 @@ def test_loading_domain_from_invalid_module(self): with pytest.raises(NoDomainException): derive_domain("dummy") + + def test_loading_domain_with_attribute_name_as_domain(self): + change_working_directory_to("test1") + + domain = derive_domain("basic") + assert domain is not None + assert domain.domain_name == "BASIC" + + def test_loading_domain_with_attribute_name_as_subdomain(self): + change_working_directory_to("test12") + + domain = derive_domain("foo12") + assert domain is not None + assert domain.domain_name == "TEST12" diff --git a/tests/cli/test_find_domain_by_string.py b/tests/cli/test_find_domain_by_string.py index bac4eb71..eb64e9f4 100644 --- a/tests/cli/test_find_domain_by_string.py +++ b/tests/cli/test_find_domain_by_string.py @@ -4,7 +4,7 @@ from protean import Domain from protean.cli import NoDomainException -from protean.utils.domain import find_domain_by_string +from protean.utils.domain_discovery import find_domain_by_string class MagicMockWithName(MagicMock): diff --git a/tests/cli/test_generate_docker_compose.py b/tests/cli/test_generate_docker_compose.py index fa946e26..bd84b60f 100644 --- a/tests/cli/test_generate_docker_compose.py +++ b/tests/cli/test_generate_docker_compose.py @@ -30,7 +30,7 @@ def test_cli_command(self): """Test the CLI command to generate a docker compose file""" change_working_directory_to("test8") - args = ["docker-compose", "sqlite_domain.py"] + args = ["docker-compose", "--domain", "sqlite_domain.py"] result = runner.invoke(app, args) print(result.output) diff --git a/tests/cli/test_shell.py b/tests/cli/test_shell.py index 98995b11..901ea921 100644 --- a/tests/cli/test_shell.py +++ b/tests/cli/test_shell.py @@ -29,7 +29,55 @@ def reset_path(self): def test_shell_command_success(self): change_working_directory_to("test7") - args = ["shell", "publishing.py"] + args = ["shell", "--domain", "publishing7.py"] + + # Run the shell command + result = runner.invoke(app, args) + + # Assertions + print(result.output) + assert result.exit_code == 0 + + def test_shell_command_with_no_explicit_domain_and_domain_py_file(self): + change_working_directory_to("test10") + + args = ["shell"] + + # Run the shell command + result = runner.invoke(app, args) + + # Assertions + print(result.output) + assert result.exit_code == 0 + + def test_shell_command_with_no_explicit_domain_and_subdomain_py_file(self): + change_working_directory_to("test11") + + args = ["shell"] + + # Run the shell command + result = runner.invoke(app, args) + + # Assertions + print(result.output) + assert result.exit_code == 0 + + def test_shell_command_with_domain_attribute_name_as_domain(self): + change_working_directory_to("test1") + + args = ["shell", "--domain", "basic"] + + # Run the shell command + result = runner.invoke(app, args) + + # Assertions + print(result.output) + assert result.exit_code == 0 + + def test_shell_command_with_domain_attribute_name_as_subdomain(self): + change_working_directory_to("test12") + + args = ["shell", "--domain", "foo12"] # Run the shell command result = runner.invoke(app, args) @@ -41,7 +89,7 @@ def test_shell_command_success(self): def test_shell_command_raises_no_domain_exception_when_no_domain_is_found(self): change_working_directory_to("test7") - args = ["shell", "foobar"] + args = ["shell", "--domain", "foobar"] # Run the shell command and expect it to raise an exception with pytest.raises(NoDomainException): diff --git a/tests/domain/test_domain_shell_context.py b/tests/domain/test_domain_shell_context.py index 23290c4b..550e2c3d 100644 --- a/tests/domain/test_domain_shell_context.py +++ b/tests/domain/test_domain_shell_context.py @@ -5,7 +5,7 @@ import pytest -from protean.utils.domain import derive_domain +from protean.utils.domain_discovery import derive_domain from tests.shared import change_working_directory_to @@ -24,7 +24,7 @@ def reset_path(self): def test_return_type(self): change_working_directory_to("test9") - domain = derive_domain("publishing:domain") + domain = derive_domain("publishing9:domain") assert domain is not None domain.init() @@ -40,9 +40,10 @@ def test_return_type(self): # Test for elements in the context assert ( "Post" in context - and context["Post"] is domain.registry.aggregates["publishing.Post"].cls + and context["Post"] is domain.registry.aggregates["publishing9.Post"].cls ), "`Post` class should be in the context" assert ( "Comment" in context - and context["Comment"] is domain.registry.entities["publishing.Comment"].cls + and context["Comment"] + is domain.registry.entities["publishing9.Comment"].cls ), "`Comment` class should be in the context" diff --git a/tests/domain/test_domain_traversal.py b/tests/domain/test_domain_traversal.py index f5840799..c6058fcf 100644 --- a/tests/domain/test_domain_traversal.py +++ b/tests/domain/test_domain_traversal.py @@ -4,15 +4,15 @@ class TestDomainTraversal: @pytest.mark.no_test_domain def test_loading_domain_without_init(self): - from tests.support.test_domains.test6 import publishing + from tests.support.test_domains.test6 import publishing6 - assert publishing.domain is not None - assert len(publishing.domain.registry.aggregates) == 0 + assert publishing6.domain is not None + assert len(publishing6.domain.registry.aggregates) == 0 @pytest.mark.no_test_domain def test_loading_domain_with_init(self): - from tests.support.test_domains.test7 import publishing + from tests.support.test_domains.test7 import publishing7 - assert publishing.domain is not None - publishing.domain.init() - assert len(publishing.domain.registry.aggregates) == 1 + assert publishing7.domain is not None + publishing7.domain.init() + assert len(publishing7.domain.registry.aggregates) == 1 diff --git a/tests/support/test_domains/README.md b/tests/support/test_domains/README.md new file mode 100644 index 00000000..29e8ea9a --- /dev/null +++ b/tests/support/test_domains/README.md @@ -0,0 +1,4 @@ +This directory encloses folders containing various configurations +of test domains. The test domains are used in various parts of +the test suite, notably in automatic discovery of domains and +loading domain instances in shell. \ No newline at end of file diff --git a/tests/support/test_domains/test10/domain.py b/tests/support/test_domains/test10/domain.py new file mode 100644 index 00000000..17e29031 --- /dev/null +++ b/tests/support/test_domains/test10/domain.py @@ -0,0 +1,7 @@ +"""Dummy Domain file to test auto loading of `domain` attribute +when no file/package and attribute name were provided +""" + +from protean.domain import Domain + +domain = Domain(__file__, "TEST10") diff --git a/tests/support/test_domains/test11/subdomain.py b/tests/support/test_domains/test11/subdomain.py new file mode 100644 index 00000000..8e2f9ab6 --- /dev/null +++ b/tests/support/test_domains/test11/subdomain.py @@ -0,0 +1,7 @@ +"""Dummy Domain file to test auto loading of domain attribute +when no file/package and attribute name were provided +""" + +from protean.domain import Domain + +domain = Domain(__file__, "TEST11") diff --git a/tests/support/test_domains/test12/foo12.py b/tests/support/test_domains/test12/foo12.py new file mode 100644 index 00000000..b93c9cc8 --- /dev/null +++ b/tests/support/test_domains/test12/foo12.py @@ -0,0 +1,6 @@ +"""Dummy Domain file to test auto loading of `subdomain` attribute""" + +from protean.domain import Domain + +subdomain = Domain(__file__, "TEST12") +specific = Domain(__file__, "TEST12_SPECIFIC") diff --git a/tests/support/test_domains/test6/post.py b/tests/support/test_domains/test6/post6.py similarity index 84% rename from tests/support/test_domains/test6/post.py rename to tests/support/test_domains/test6/post6.py index cd482df2..95b66e32 100644 --- a/tests/support/test_domains/test6/post.py +++ b/tests/support/test_domains/test6/post6.py @@ -1,7 +1,7 @@ from datetime import datetime from protean.fields import DateTime, HasMany, Reference, String -from tests.support.test_domains.test6.publishing import domain +from tests.support.test_domains.test6.publishing6 import domain @domain.aggregate diff --git a/tests/support/test_domains/test6/publishing.py b/tests/support/test_domains/test6/publishing6.py similarity index 100% rename from tests/support/test_domains/test6/publishing.py rename to tests/support/test_domains/test6/publishing6.py diff --git a/tests/support/test_domains/test7/post.py b/tests/support/test_domains/test7/post7.py similarity index 84% rename from tests/support/test_domains/test7/post.py rename to tests/support/test_domains/test7/post7.py index 266a6bda..1ddf3377 100644 --- a/tests/support/test_domains/test7/post.py +++ b/tests/support/test_domains/test7/post7.py @@ -1,7 +1,7 @@ from datetime import datetime from protean.fields import DateTime, HasMany, Reference, String -from tests.support.test_domains.test7.publishing import domain +from tests.support.test_domains.test7.publishing7 import domain @domain.aggregate diff --git a/tests/support/test_domains/test7/publishing.py b/tests/support/test_domains/test7/publishing7.py similarity index 100% rename from tests/support/test_domains/test7/publishing.py rename to tests/support/test_domains/test7/publishing7.py diff --git a/tests/support/test_domains/test8/sqlite_domain.py b/tests/support/test_domains/test8/sqlite_domain.py index 1068fdb1..ab944930 100644 --- a/tests/support/test_domains/test8/sqlite_domain.py +++ b/tests/support/test_domains/test8/sqlite_domain.py @@ -1,3 +1,7 @@ +"""Dummy Domain file to test domain loading with config +for further testing, like docker file generation +""" + from protean.domain import Domain domain = Domain(__file__, "SQLite-Domain") diff --git a/tests/support/test_domains/test9/publishing.py b/tests/support/test_domains/test9/publishing9.py similarity index 85% rename from tests/support/test_domains/test9/publishing.py rename to tests/support/test_domains/test9/publishing9.py index 6f3aa8c3..d2d4fcc2 100644 --- a/tests/support/test_domains/test9/publishing.py +++ b/tests/support/test_domains/test9/publishing9.py @@ -1,4 +1,4 @@ -"""A simple domain module with domain elements in the same file""" +"""A simple dummy domain module with domain elements in the same file""" from datetime import datetime diff --git a/tests/utils/support/domain.py b/tests/utils/support/domain.py deleted file mode 100644 index 3b853a2c..00000000 --- a/tests/utils/support/domain.py +++ /dev/null @@ -1,3 +0,0 @@ -from protean.domain import Domain - -publishing = Domain(__file__, "Publishing Domain") diff --git a/tests/utils/test_get_version.py b/tests/utils/test_get_version.py deleted file mode 100644 index 65e72187..00000000 --- a/tests/utils/test_get_version.py +++ /dev/null @@ -1,7 +0,0 @@ -import importlib - -from protean.utils import get_version - - -def test_get_version(): - assert get_version() == importlib.metadata.version("protean") diff --git a/tests/utils/test_import_from_full_path.py b/tests/utils/test_import_from_full_path.py deleted file mode 100644 index 336de7a6..00000000 --- a/tests/utils/test_import_from_full_path.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from protean.utils import import_from_full_path - - -def test_that_a_domain_can_be_imported_from_a_full_path(): - domain = import_from_full_path("publishing", "tests/utils/support/domain.py") - - assert domain is not None - assert domain.domain_name == "Publishing Domain" - - -def test_that_an_invalid_file_path_throws_exception(): - with pytest.raises(FileNotFoundError): - import_from_full_path("publishing", "dummy_path/domain.py") diff --git a/tests/utils/test_util.py b/tests/utils/test_util.py deleted file mode 100644 index 56856d4c..00000000 --- a/tests/utils/test_util.py +++ /dev/null @@ -1,13 +0,0 @@ -import datetime - -from protean.utils import utcnow_func - - -def test_utcnow_func(): - func = utcnow_func - assert func is not None - assert callable(func) is True - - result = func() - assert result is not None - assert type(result) is datetime.datetime