Skip to content

Commit

Permalink
improve hatchling plugin code
Browse files Browse the repository at this point in the history
  • Loading branch information
carderne committed Aug 18, 2024
1 parent 7cb54bf commit a359360
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 40 deletions.
53 changes: 38 additions & 15 deletions plugins/hatch/hatch_una/hatch_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,57 @@


class UnaBuildHook(BuildHookInterface[BuilderConfig]):
"""
Force-include all needed internal monorepo dependencies.
"""

PLUGIN_NAME = "una-build"

def initialize(self, version: str, build_data: dict[str, Any]) -> None:
print("una: Injecting internal dependencies")

# load the config for this app/project
path = Path(self.root)
conf = util.load_conf(path)
name: str = conf["project"]["name"]

try:
int_deps: dict[str, str] = conf["tool"]["una"]["libs"]
except KeyError as e:
raise KeyError(
f"App/project '{name}' is missing '[tool.una.libs]' in pyproject.toml"
) from e

# need to determine workspace style (packages or modules)
# as packages style needs dependencies' pyproject.tomls to be included
# so that they're available in src -> sdist -> wheel builds
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"]
style = util.get_workspace_style(root_path)

if not int_deps:
if style == "packages":
# this is fine, the app doesn't import anything internally
return
else:
# this is an empty project, useless and accidental
raise ValueError(f"Project '{name}' has no dependencies")

int_deps: dict[str, str] = conf["tool"]["una"]["libs"]
# make sure all int_deps exist
found = [Path(k) for k in int_deps if (path / k).exists()]
if not int_deps or not found:
# should I raise here?
return
missing = set(int_deps) - set(str(p) for p in found)
if len(missing) > 0:
missing_str = ", ".join(missing)
raise ValueError(f"Could not find these paths: {missing_str}")

add_root_pyproj = {str(root_path / util.PYPROJ): str(util.EXTRA_PYPROJ / "root" / util.PYPROJ)}
# need to add the root workspace pyproject.toml so that in src -> sdist -> wheel builds,
# we can still determine the style (for packages style)
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
str(f.parents[1] / util.PYPROJ): str(util.EXTRA_PYPROJ / f.name / util.PYPROJ)
for f in found
}
else:
add_packages_pyproj = {}
Expand Down
43 changes: 36 additions & 7 deletions plugins/hatch/hatch_una/hatch_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,59 @@


class UnaMetaHook(MetadataHookInterface):
"""
Inject needed third-party dependencies into project.dependencies.
This hook is only needed for Packages style una workspaces.
Modules style should have all dependencies specified in pyproject.toml.
"""

PLUGIN_NAME = "una-meta"

def update(self, metadata: dict[str, Any]) -> None:
print("una: Injecting transitive external dependencies")
root = Path(self.root)
conf = util.load_conf(root)
int_deps: dict[str, str] = conf["tool"]["una"]["libs"]

project_deps: list[str] = metadata.get("dependencies", {})
# load the config for this app/project
path = Path(self.root)
conf = util.load_conf(path)
name: str = conf["project"]["name"]

root_path = path.parents[1]
style = util.get_workspace_style(root_path)
if style == "modules":
raise ValueError("Hook 'una-meta' should not be used with Modules style workspace")

try:
int_deps: dict[str, str] = conf["tool"]["una"]["libs"]
except KeyError as e:
raise KeyError(
f"App/project '{name}' is missing '[tool.una.libs]' in pyproject.toml"
) from e

project_deps: list[str] = metadata.get("dependencies", [])
project_deps = [d.strip().replace(" ", "") for d in project_deps]

add_deps: list[str] = []
for dep_path in int_deps:
# In builds that do src -> sdist -> wheel, the needed pyproject.toml files
# will have been copied into the sdist so they're available for the wheel build.
# Here we check for both in order.
dep_project_path = Path(dep_path).parents[1]
extra_path = util.EXTRA_PYPROJ / Path(dep_path).name
if dep_project_path.exists():
use_path = dep_project_path
elif extra_path.exists():
use_path = extra_path
else:
# should I raise here?
continue
raise ValueError(f"Could not find internal dependency at '{dep_path}'")

# load all third-party dependencies from this internal dependency into the
# project.dependencies table
dep_conf = util.load_conf(use_path)
dep_deps: list[str] = dep_conf["project"]["dependencies"]
try:
dep_deps: list[str] = dep_conf["project"]["dependencies"]
except KeyError as e:
raise KeyError(f"No project.dependcies table for '{use_path}'")
dep_deps = [d.strip().replace(" ", "") for d in dep_deps]
add_deps.extend(dep_deps)

Expand Down
32 changes: 31 additions & 1 deletion plugins/hatch/hatch_una/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import tomllib
from pathlib import Path
from typing import Any
from typing import Any, Literal, TypeAlias

PYPROJ = "pyproject.toml"
EXTRA_PYPROJ = Path("extra_pyproj")
Expand All @@ -9,3 +9,33 @@
def load_conf(path: Path) -> dict[str, Any]:
with (path / PYPROJ).open("rb") as fp:
return tomllib.load(fp)


Style: TypeAlias = Literal["packages", "modules"]


def get_workspace_style(root_path: Path) -> Style:
"""
Get the root workspace style
Param `path` should be the path to the ap/project,
NOT to the root workspace.
"""
# In builds that do src -> sdist -> wheel, the root pyproject.toml file will
# have been copied into the sdist so available for the wheel build.
# Here we check for both in order.
extra_root_path = EXTRA_PYPROJ / "root"
if (root_path / PYPROJ).exists():
use_root_path = root_path
elif (extra_root_path / PYPROJ).exists():
use_root_path = extra_root_path
else:
raise ValueError("No root pyproject to determine workspace style")
root_conf = load_conf(use_root_path)
try:
style: Style = root_conf["tool"]["una"]["style"]
return style
except KeyError as e:
raise KeyError(
"Root workspace pyproject.toml needs '[tool.una]' with style specified"
) from e
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ source = "vcs"

[tool.ruff]
target-version = "py311"
line-length = 120
line-length = 100

[tool.ruff.lint]
select = ["A", "E", "F", "I", "N", "T100", "UP", "ANN401"]
Expand Down
6 changes: 5 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
@pytest.fixture
def use_fake(monkeypatch: MonkeyPatch):
def patch():
monkeypatch.setattr(config, "load_conf", lambda _: config.load_conf_from_str(BASE_PYPROJECT)) # type: ignore[reportUnknownArgumentType]
monkeypatch.setattr(
config,
"load_conf",
lambda _: config.load_conf_from_str(BASE_PYPROJECT), # type: ignore[reportUnknownArgumentType]
)

return patch

Expand Down
2 changes: 1 addition & 1 deletion una/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
help="Commands for creating a workspace, apps, libs and projects.",
)

option_alias = Option(help="alias for third-party libraries, useful when an import differ from the library name") # type: ignore[reportAny]
option_alias = Option(help="alias for third-party libraries, map install to import name") # type: ignore[reportAny]
option_verbose = Option(help="More verbose output.") # type: ignore[reportAny]
option_quiet = Option(help="Do not output any messages.") # type: ignore[reportAny]

Expand Down
12 changes: 9 additions & 3 deletions una/differ.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,18 @@ def parse_folder_parts(pattern: str, changed_file: Path) -> str:


def get_changed(pattern: str, changed_files: list[Path]) -> set[str]:
return {parse_folder_parts(pattern, f) for f in changed_files if re.match(pattern, f.as_posix())}
return {
parse_folder_parts(pattern, f) for f in changed_files if re.match(pattern, f.as_posix())
}


def parse_path_pattern(_: Path, top_dir: str, namespace: str) -> str:
return f"{top_dir}/{namespace}/"


def get_changed_int_deps(root: Path, top_dir: str, changed_files: list[Path], namespace: str) -> list[str]:
def get_changed_int_deps(
root: Path, top_dir: str, changed_files: list[Path], namespace: str
) -> list[str]:
pattern = parse_path_pattern(root, top_dir, namespace)
return sorted(get_changed(pattern, changed_files))

Expand Down Expand Up @@ -87,7 +91,9 @@ def print_diff_details(projects_data: list[Proj], apps: list[str], libs: list[st
if not apps and not libs:
return
console = Console(theme=defaults.una_theme)
table = internal_deps.build_int_deps_in_projects_table(projects_data, apps, libs, for_info=False)
table = internal_deps.build_int_deps_in_projects_table(
projects_data, apps, libs, for_info=False
)
console.print(table, overflow="ellipsis")


Expand Down
7 changes: 5 additions & 2 deletions una/distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ def extract_library_names(deps: ExtDeps) -> set[str]:


def known_aliases_and_sub_dependencies(deps: ExtDeps, library_alias: list[str]) -> set[str]:
"""Collect known aliases (packages) for third-party libraries.
"""
Collect known aliases (packages) for third-party libraries.
When the library origin is not from a lock-file:
collect sub-dependencies and distribution top-namespace for each library, and append to the result.
collect sub-dependencies and distribution top-namespace for each library,
and append to the result.
"""
lock_file = any(str.endswith(deps.source, s) for s in {".lock", ".txt"})
third_party_libs = extract_library_names(deps)
Expand Down
3 changes: 2 additions & 1 deletion una/external_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ def print_missing_installed_libs(
console = Console(theme=defaults.una_theme)
missing = ", ".join(sorted(diff))
console.print(
f"[data]Could not locate all libraries in [/][proj]{project_name}[/]. [data]Caused by missing dependencies?[/]"
f"[data]Could not locate all libraries in [/][proj]{project_name}[/].",
"[data]Caused by missing dependencies?[/]",
)
console.print(f":thinking_face: {missing}")
return False
Expand Down
12 changes: 9 additions & 3 deletions una/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ def create_project(path: Path, name: str, dependencies: str, from_app: str) -> N
proj_dir,
defaults.pyproj,
content
+ defaults.example_modules_style_project_deps.format(ns=ns, app_name=from_app, lib_name=defaults.example_lib),
+ defaults.example_modules_style_project_deps.format(
ns=ns, app_name=from_app, lib_name=defaults.example_lib
),
)


Expand All @@ -137,14 +139,18 @@ def create_package(path: Path, name: str, top_dir: str, content: str, dependenci
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))
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)
pyproj_content += defaults.example_packages_style_app_deps.format(
ns=ns, lib_name=defaults.example_lib
)
create_file(
app_dir,
defaults.pyproj,
Expand Down
22 changes: 17 additions & 5 deletions una/internal_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ def flatten_import(acc: set[str], kv: tuple[str, set[str]]) -> set[str]:


def flatten_imports(int_dep_imports: Imports) -> set[str]:
"""Flatten the dict into a set of imports, with the actual int_dep filtered away when existing as an import"""
"""
Flatten the dict into a set of imports.
The actual int_dep filtered away when existing as an import.
"""
return reduce(flatten_import, int_dep_imports.items(), set())


Expand All @@ -77,7 +81,9 @@ def create_columns(imported_apps: list[str], imported_libs: list[str]) -> list[s
return lib_cols + app_cols


def create_rows(apps: set[str], libs: set[str], import_data: Imports, imported: list[str]) -> list[list[str]]:
def create_rows(
apps: set[str], libs: set[str], import_data: Imports, imported: list[str]
) -> list[list[str]]:
app_rows = [to_row(b, "app", import_data, imported) for b in sorted(apps)]
lib_rows = [to_row(c, "lib", import_data, imported) for c in sorted(libs)]
return lib_rows + app_rows
Expand Down Expand Up @@ -117,7 +123,9 @@ def get_project_int_deps(
return IntDeps(libs=libs_in_project, apps=apps_in_project)


def get_int_deps_in_projects(root: Path, libs_paths: list[str], apps_paths: list[str], namespace: str) -> list[Proj]:
def get_int_deps_in_projects(
root: Path, libs_paths: list[str], apps_paths: list[str], namespace: str
) -> list[Proj]:
packages = files.get_projects(root)
ws_root = config.get_workspace_root()
style = config.get_style(ws_root)
Expand Down Expand Up @@ -170,11 +178,15 @@ def build_int_deps_in_projects_table(
for col in proj_cols:
table.add_column(col, justify="center")
for int_dep in sorted(libs):
statuses = [int_dep_status_projects(int_dep, p.int_deps.libs, for_info) for p in projects_data]
statuses = [
int_dep_status_projects(int_dep, p.int_deps.libs, for_info) for p in projects_data
]
cols = [f"[lib]{int_dep}[/]"] + statuses
table.add_row(*cols)
for int_dep in sorted(apps):
statuses = [int_dep_status_projects(int_dep, p.int_deps.apps, for_info) for p in projects_data]
statuses = [
int_dep_status_projects(int_dep, p.int_deps.apps, for_info) for p in projects_data
]
cols = [f"[app]{int_dep}[/]"] + statuses
table.add_row(*cols)
return table

0 comments on commit a359360

Please sign in to comment.