Skip to content

Commit

Permalink
QPT-37223 New nspoet CLI (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdrake committed Nov 11, 2021
1 parent 710f507 commit 0987e61
Show file tree
Hide file tree
Showing 21 changed files with 2,840 additions and 23 deletions.
365 changes: 365 additions & 0 deletions .circleci/config.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Expand Up @@ -74,7 +74,7 @@ repos:
entry: mypy -p ns_poet
language: python
types: [ file, python ]
additional_dependencies: [ mypy==0.910 ]
additional_dependencies: [ mypy==0.910, types-toml, types-setuptools ]
pass_filenames: false

- id: flake8
Expand Down
2 changes: 1 addition & 1 deletion ns_poet/__init__.py
@@ -1,3 +1,3 @@
"""Autogenerate Poetry package manifests in a monorepo"""

__version__ = "0.1.0"
__version__ = "0.2.0"
57 changes: 57 additions & 0 deletions ns_poet/cli.py
@@ -0,0 +1,57 @@
"""Contains the CLI"""

import logging
from pathlib import Path
from typing import Optional

import click

from ns_poet.processor import PackageProcessor
from ns_poet.project import PROJECT_CONFIG
from ns_poet.requirements import update_import_map

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@click.group()
def cli() -> None:
"""Autogenerate Poetry package manifests in a monorepo"""
pass


@cli.group(name="import-map")
def import_map() -> None:
"""Commands for managing imports"""
pass


@import_map.command()
def update() -> None:
"""Update an import map from requirements.txt"""
update_import_map()


@cli.group()
def package() -> None:
"""Commands for managing packages"""
pass


@package.command()
@click.option(
"-p",
"--package-path",
type=click.Path(exists=True, dir_okay=True, file_okay=False, path_type=Path),
help="Generate a package manifest for a single package path",
)
def generate(package_path: Optional[Path]) -> None:
"""Generate Poetry package manifests"""
PROJECT_CONFIG.load_requirements()
processor = PackageProcessor()
processor.register_packages()
processor.ensure_no_circular_imports()
if package_path:
processor.generate_package_manifest(package_path)
else:
processor.generate_package_manifests()
6 changes: 0 additions & 6 deletions ns_poet/example.py

This file was deleted.

67 changes: 67 additions & 0 deletions ns_poet/exceptions.py
@@ -0,0 +1,67 @@
"""Contains exception classes"""


class CircularImportsFound(Exception):
"""Raised when circular imports were found processing build targets"""

pass


class NoBuildDependenciesFound(Exception):
"""Raised when the dependencies argument could not be found in a BUILD file AST"""

pass


class DistributionError(Exception):
"""Raised when a Python distribution can't be parsed"""

pass


class UnsupportedDistributionFormat(DistributionError):
"""Raised when the distribution file is something other than a .whl or .tar.gz"""

pass


class MissingDistributionMetadataFile(DistributionError):
"""Raised when there is a missing metadata file in a .whl or .tar.gz"""

pass


class NoProjectName(Exception):
"""Raised when no project name could be parsed from distribution metadata"""

pass


class InvalidTopLevelFile(Exception):
"""Raised when a top_level.txt file could not be interpreted"""

pass


class MultipleSourcePackagesFound(Exception):
"""Raised when more than one package is found in src/"""

pass


class DuplicateTarget(Exception):
"""Raised when attempting to register a target that has an existing key"""

pass


class NoConsoleScriptFound(Exception):
"""Raised when no console_scripts are found in setup.py for a binary target"""

pass


class NoTargetFound(Exception):
"""Raised when no target could be found in the registered graph of targets"""

pass
111 changes: 111 additions & 0 deletions ns_poet/package.py
@@ -0,0 +1,111 @@
"""Contains PoetPackage class"""

from pathlib import Path
from typing import Any, Dict, MutableMapping, Optional

import toml


class PoetPackage:
"""Class that represents a package managed by ns-poet
This object manages a Python package's pyproject.toml (i.e. manifest) file.
The schema for the configuration file in the git project root is::
[tool.nspoet]
generate_package_manifest = true
"""

def __init__(self, package_path: Path) -> None:
"""Initializer
Args:
package_path: Path to the package
"""
self.package_path = package_path
self.config_file_path = package_path.joinpath("pyproject.toml")
self._config: Optional[MutableMapping[str, Any]] = None

@classmethod
def from_path(cls, package_path: Path) -> "PoetPackage":
"""Create a package object and load configuration from a given package path
Args:
package_path: Path to the package
Returns:
new PoetPackage instance
"""
p = cls(package_path)
p.load_config()
return p

def load_config(self) -> MutableMapping[str, Any]:
"""Load configuration from disk
Returns:
configuration dict
"""
if self.config_file_path.is_file():
with self.config_file_path.open() as f:
self._config = toml.load(f)
else:
self._config = {}

return self._config

def save_config(self) -> None:
"""Save the configuration object to disk"""
if self.generate_package_manifest:
with self.config_file_path.open("w") as f:
toml.dump( # type: ignore
self._config,
f,
encoder=toml.encoder.TomlPreserveInlineDictEncoder(), # type: ignore
)

def to_string(self) -> str:
"""Dump the configuration to a TOML string"""
return toml.dumps( # type: ignore
self._config, encoder=toml.encoder.TomlPreserveInlineDictEncoder() # type: ignore
)

@property
def package_config(self) -> Dict[str, Any]:
"""Return the nspoet configuration subsection within the manifest"""
return self._config.get("tool", {}).get("nspoet", {})

@property
def generate_package_manifest(self) -> bool:
"""Flag denoting whether to generate a package manifest file"""
return self.package_config.get("generate_package_manifest", True)

def update(
self, name: str, dependencies: Dict[str, str], dev_dependencies: Dict[str, str]
) -> None:
"""Update the package configuration in place
Args:
name: package name
dependencies: map of dependency name to version specifier
dev_dependencies: map of development dependency name to version specifier
"""
self._config.setdefault("tool", {})
self._config["tool"].setdefault("poetry", {})
self._config["tool"]["poetry"]["name"] = name
self._config["tool"]["poetry"]["version"] = "1.0.0"
self._config["tool"]["poetry"]["description"] = ""
self._config["tool"]["poetry"]["authors"] = []
self._config["tool"]["poetry"]["license"] = "Proprietary"
self._config["tool"]["poetry"]["dependencies"] = dependencies
self._config["tool"]["poetry"]["dev-dependencies"] = dev_dependencies
self._config["build-system"] = {
"requires": ["poetry-core>=1.0.0"],
"build-backend": "poetry.core.masonry.api",
}
4 changes: 4 additions & 0 deletions ns_poet/package_targets/__init__.py
@@ -0,0 +1,4 @@
"""Defines subpackage exports"""
from .base import BuildTarget
from .python_package import PythonPackage
from .requirement import PythonRequirement
46 changes: 46 additions & 0 deletions ns_poet/package_targets/base.py
@@ -0,0 +1,46 @@
"""Contains base BuildTarget class"""
from abc import ABC
from functools import total_ordering
from typing import Any


@total_ordering
class BuildTarget(ABC):
"""Represents a Python build target in Pants.
This should be used as a mixin to provide common attributes and methods.
"""

# This string will be used as an identifier for the build target
key: str = None

def __str__(self) -> str:
"""String representation"""
return str(self.key)

def __eq__(self, other: Any) -> bool:
"""Equality check"""
if hasattr(other, "key"):
return str(self.key) == str(other.key)
else:
return False

def __hash__(self) -> int:
"""Object hash"""
return hash((str(self.key),))

def __repr__(self) -> str:
"""Object representation for debugging"""
return str(self.key)

def __lt__(self, other: Any) -> bool:
"""Less than comparator"""
if hasattr(other, "key"):
return str(self.key) < str(other.key)
else:
return False

@property
def dependency_target(self) -> str:
"""Returns the representation of this target in another target's dependencies"""
return str(self.key)

0 comments on commit 0987e61

Please sign in to comment.