Skip to content

Commit

Permalink
fix modules style handling
Browse files Browse the repository at this point in the history
  • Loading branch information
carderne committed Aug 17, 2024
1 parent 06d3e4b commit a325e36
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 17 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ But I find the [Polylith](https://polylith.gitbook.io/polylith) architecture to
It just tries to make a simple monorepo build just about possible.

una allows two directory structures or styles:
- `packages`: this is the lightest-touch approach, that is just some extra build help on top of a Rye workspace.
- `packages`: this is the default style, that is just some extra build help on top of a Rye workspace.
- `modules`: a more novel approach with just a single pyproject.toml, arguably better DevX and doesn't require a Rye workspace.

Within this context, we use the following words frequently:
- `lib`: a module or package that will be imported but not run.
- `app`: a module or package that will be run but never imported.
- `project`: a package with no code but only dependencies (only used in the `modules` style)

## Examples
You can see examples for each of the two styles here:
- [carderne/una-example-packages](https://github.com/carderne/una-example-packages)
- [carderne/una-example-modules](https://github.com/carderne/una-example-modules)

## Quickstart
This will give you a quick view of how this all works.
A `packages` style will be used by default, as it is probably more familiar to most.
Expand Down
33 changes: 28 additions & 5 deletions plugins/hatch/hatch_una/hatch_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,40 @@ class UnaBuildHook(BuildHookInterface[BuilderConfig]):

def initialize(self, version: str, build_data: dict[str, Any]) -> None:
print("una: Injecting internal dependencies")
root = Path(self.root)
conf = util.load_conf(root)
path = Path(self.root)
conf = util.load_conf(path)

root_path = path.parents[1]
extra_root_path = util.EXTRA_PYPROJ / "root"
if (root_path / util.PYPROJ).exists():
use_root_path = root_path
elif (extra_root_path / util.PYPROJ).exists():
use_root_path = extra_root_path
else:
raise ValueError("No root pyproject to determine workspace style")
root_conf = util.load_conf(use_root_path)
style: str = root_conf["tool"]["una"]["style"]

int_deps: dict[str, str] = conf["tool"]["una"]["libs"]
found = [Path(k) for k in int_deps if (root / k).exists()]
found = [Path(k) for k in int_deps if (path / k).exists()]
if not int_deps or not found:
# should I raise here?
return

add_pyproj = {str(f.parents[1] / util.PYPROJ): str(util.EXTRA_PYPROJ / f.name / util.PYPROJ) for f in found}
build_data["force_include"] = {**build_data["force_include"], **int_deps, **add_pyproj}
add_root_pyproj = {str(root_path / util.PYPROJ): str(util.EXTRA_PYPROJ / "root" / util.PYPROJ)}
if style == "packages":
add_packages_pyproj = {
str(f.parents[1] / util.PYPROJ): str(util.EXTRA_PYPROJ / f.name / util.PYPROJ) for f in found
}
else:
add_packages_pyproj = {}

build_data["force_include"] = {
**build_data["force_include"],
**int_deps,
**add_root_pyproj,
**add_packages_pyproj,
}


@hookimpl
Expand Down
18 changes: 18 additions & 0 deletions una/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ def app_command(
console.print(f"Created app {name}")


@create.command("project")
def project_command(
name: Annotated[str, Argument(help="Name of the projectlib.")],
from_app: Annotated[str, Argument(help="Name of the app the project will use.")],
):
"""Creates an Una project."""
root = config.get_workspace_root()
style = config.get_style(root)
console = Console(theme=defaults.una_theme)
if style == Style.packages:
console.print("You can't create projects in a Packages style workspace")
exit(1)

files.create_project(root, name, "", from_app)
console.print("Success!")
console.print(f"Created project {name}")


@create.command("workspace")
def workspace_command(
style: Annotated[Style, Option(help="Workspace style")] = "packages", # type:ignore[reportArgumentType]
Expand Down
38 changes: 36 additions & 2 deletions una/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
example_app = "printer"
example_lib = "greeter"

example_import = "cowsay-python==1.0.2"

packages_pyproj = """\
[project]
name = "{name}"
Expand Down Expand Up @@ -48,6 +50,39 @@
[tool.una.libs]
"""

projects_pyproj = """\
[project]
name = "{name}"
version = "0.1.0"
description = ""
authors = []
dependencies = [{dependencies}]
requires-python = "{python_version}"
[build-system]
requires = ["hatchling", "hatch-una"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build]
packages = ["{ns}"]
[tool.hatch.build.hooks.una-build]
[tool.una.libs]
"""

example_packages_style_app_deps = """\
"../../libs/{lib_name}/{ns}/{lib_name}" = "{ns}/{lib_name}"
"""

example_modules_style_project_deps = """\
"../../apps/{ns}/{app_name}" = "{ns}/{app_name}"
"../../libs/{ns}/{lib_name}" = "{ns}/{lib_name}"
"""

app_template = """\
import {ns}.{lib_name} as {lib_name}
Expand All @@ -56,11 +91,10 @@ def run() -> None:
print({lib_name}.greet())
"""

lib_dependencies = "cowsay==6.1"

lib_template = """\
import cowsay
def greet() -> str:
return cowsay.say("Hello from una!")
"""
Expand Down
33 changes: 24 additions & 9 deletions una/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ def collect_libs_paths(root: Path, ns: str, libs: set[str]) -> set[Path]:
return collect_paths(root, ns, defaults.libs_dir, libs)


def update_workspace_config(path: Path, ns: str, style: Style) -> None:
def update_workspace_config(path: Path, ns: str, style: Style, dependencies: str) -> None:
pyproj = path / defaults.pyproj
if style == Style.modules:
with pyproj.open() as f:
toml = tomlkit.parse(f.read())
toml["project"]["dependencies"].append(dependencies) # type:ignore[reportIndexIssues]
toml["tool"]["rye"]["workspace"] = {"member": ["projects/*"]} # type:ignore[reportIndexIssues]
toml["tool"]["hatch"]["build"].add("dev-mode-dirs", ["libs", "apps"]) # type:ignore[reportIndexIssues]
toml["tool"]["una"] = {"style": "modules"} # type:ignore[reportIndexIssues]
with pyproj.open("w", encoding="utf-8") as f:
Expand All @@ -84,37 +86,41 @@ def update_workspace_config(path: Path, ns: str, style: Style) -> None:


def create_workspace(path: Path, ns: str, style: Style) -> None:
update_workspace_config(path, ns, style)

app_content = defaults.app_template.format(ns=ns, lib_name=defaults.example_lib)
lib_content = defaults.lib_template
dependencies = '"cowsay-python==1.0.1"'
dependencies = f'"{defaults.example_import}"'

update_workspace_config(path, ns, style, defaults.example_import)
if style == Style.modules:
create_project(path, "example_project", dependencies)
create_project(path, "example_project", dependencies, defaults.example_app)

if style == Style.packages:
create_package(path, defaults.example_app, defaults.apps_dir, app_content, "")
create_package(path, defaults.example_lib, defaults.libs_dir, lib_content, dependencies)
else:
create_module(path, defaults.example_app, defaults.apps_dir, app_content)
create_module(path, defaults.example_lib, defaults.libs_dir, app_content)
create_module(path, defaults.example_lib, defaults.libs_dir, lib_content)


def parse_package_paths(packages: list[Include]) -> list[Path]:
sorted_packages = sorted(packages, key=lambda p: p.src)
return [Path(p.src) for p in sorted_packages]


def create_project(path: Path, name: str, dependencies: str) -> None:
def create_project(path: Path, name: str, dependencies: str, from_app: str) -> None:
conf = config.load_conf(path)
python_version = conf.project.requires_python
ns = config.get_ns(path)

proj_dir = create_dir(path, f"projects/{name}")
content = defaults.projects_pyproj.format(
ns=ns, name=name, python_version=python_version, dependencies=dependencies
)
create_file(
proj_dir,
defaults.pyproj,
defaults.packages_pyproj.format(name=name, python_version=python_version, dependencies=""),
content
+ defaults.example_modules_style_project_deps.format(ns=ns, app_name=from_app, lib_name=defaults.example_lib),
)


Expand All @@ -124,16 +130,25 @@ def create_package(path: Path, name: str, top_dir: str, content: str, dependenci
ns = config.get_ns(path)

app_dir = create_dir(path, f"{top_dir}/{name}")
ns_dir = create_dir(path, f"{top_dir}/{name}/{ns}")
code_dir = create_dir(path, f"{top_dir}/{name}/{ns}/{name}")
test_dir = create_dir(path, f"{top_dir}/{name}/tests")

create_file(code_dir, "__init__.py", content)
create_file(code_dir, "py.typed")
is_app = top_dir == defaults.apps_dir # TODO fix this hack
create_file(test_dir, f"test_{name}_import.py", content=defaults.test_template.format(ns=ns, name=name))
pyproj_content = defaults.packages_pyproj.format(
name=name, python_version=python_version, dependencies=dependencies
)
if is_app:
create_file(ns_dir, "py.typed") # is this necessary? basedpyright thinks so...
if content:
pyproj_content += defaults.example_packages_style_app_deps.format(ns=ns, lib_name=defaults.example_lib)
create_file(
app_dir,
defaults.pyproj,
content=defaults.packages_pyproj.format(name=name, python_version=python_version, dependencies=dependencies),
content,
)


Expand Down

0 comments on commit a325e36

Please sign in to comment.