Skip to content

Commit

Permalink
Merge pull request #43 from cidrblock/fix_move_collection_out_of_config
Browse files Browse the repository at this point in the history
Refactor collection, add feedback
  • Loading branch information
cidrblock authored Sep 15, 2023
2 parents d8ee36c + 192a485 commit 957373b
Show file tree
Hide file tree
Showing 13 changed files with 513 additions and 286 deletions.
3 changes: 3 additions & 0 deletions .config/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ bindir
bthornto
caplog
capsys
cauthor
cdescription
cnamespace
cpart
cpath
crepository
fileh
fqcn
levelname
Expand Down
3 changes: 1 addition & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@
"mypy.targets": ["src", "tests"],
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"python.formatting.provider": "none"
}
}
11 changes: 7 additions & 4 deletions src/pip4a/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,13 @@ def run(self: Cli) -> None:
"""Run the application."""
logging.getLogger("pip4a")

term_features = TermFeatures(
color=False if os.environ.get("NO_COLOR") else not self.args.no_ansi,
links=not self.args.no_ansi,
)
if not sys.stdout.isatty():
term_features = TermFeatures(color=False, links=False)
else:
term_features = TermFeatures(
color=False if os.environ.get("NO_COLOR") else not self.args.no_ansi,
links=not self.args.no_ansi,
)

self.config = Config(args=self.args, term_features=term_features)
self.config.init()
Expand Down
181 changes: 181 additions & 0 deletions src/pip4a/collection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""A collection abstraction."""
from __future__ import annotations

import logging
import re
import sys

from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

import yaml

from .utils import hint


if TYPE_CHECKING:
from .config import Config


logger = logging.getLogger(__name__)


@dataclass
class Collection:
"""A collection request specification."""

config: Config
path: Path | None = None
opt_deps: str | None = None
local: bool | None = None
cnamespace: str | None = None
cname: str | None = None
specifier: str | None = None

@property
def name(self: Collection) -> str:
"""Return the collection name."""
return f"{self.cnamespace}.{self.cname}"

@property
def cache_dir(self: Collection) -> Path:
"""Return the collection cache directory."""
collection_cache_dir = self.config.venv_cache_dir / self.name
if not collection_cache_dir.exists():
collection_cache_dir.mkdir()
return collection_cache_dir

@property
def build_dir(self: Collection) -> Path:
"""Return the collection cache directory."""
collection_build_dir = self.cache_dir / "build"
if not collection_build_dir.exists():
collection_build_dir.mkdir()
return collection_build_dir

@property
def site_pkg_path(self: Collection) -> Path:
"""Return the site packages collection path."""
if not self.cnamespace or not self.cname:
msg = "Collection namespace or name not set."
raise RuntimeError(msg)
return self.config.site_pkg_collections_path / self.cnamespace / self.cname


def parse_collection_request( # noqa: PLR0915
string: str,
config: Config,
) -> Collection:
"""Parse a collection request str."""
collection = Collection(config=config)
# spec with dep, local
if "[" in string and "]" in string:
msg = f"Found optional dependencies in collection request: {string}"
logger.debug(msg)
path = Path(string.split("[")[0]).expanduser().resolve()
if not path.exists():
msg = "Provide an existing path to a collection when specifying optional dependencies."
hint(msg)
msg = f"Failed to find collection path: {path}"
logger.critical(msg)
msg = f"Found local collection request with dependencies: {string}"
logger.debug(msg)
collection.path = path
msg = f"Setting collection path: {collection.path}"
collection.opt_deps = string.split("[")[1].split("]")[0]
msg = f"Setting optional dependencies: {collection.opt_deps}"
logger.debug(msg)
collection.local = True
msg = "Setting request as local"
logger.debug(msg)
get_galaxy(collection)
return collection
# spec without dep, local
path = Path(string).expanduser().resolve()
if path.exists():
msg = f"Found local collection request without dependencies: {string}"
logger.debug(msg)
msg = f"Setting collection path: {path}"
logger.debug(msg)
collection.path = path
msg = "Setting request as local"
logger.debug(msg)
collection.local = True
get_galaxy(collection)
return collection
non_local_re = re.compile(
r"""
(?P<cnamespace>[A-Za-z0-9]+) # collection name
\. # dot
(?P<cname>[A-Za-z0-9]+) # collection name
(?P<specifier>[^A-Za-z0-9].*)? # optional specifier
""",
re.VERBOSE,
)
matched = non_local_re.match(string)
if not matched:
msg = (
"Specify a valid collection name (ns.n) with an optional version specifier"
)
hint(msg)
msg = f"Failed to parse collection request: {string}"
logger.critical(msg)
sys.exit(1)
msg = f"Found non-local collection request: {string}"
logger.debug(msg)

collection.cnamespace = matched.group("cnamespace")
msg = f"Setting collection namespace: {collection.cnamespace}"
logger.debug(msg)

collection.cname = matched.group("cname")
msg = f"Setting collection name: {collection.cname}"
logger.debug(msg)

if matched.group("specifier"):
collection.specifier = matched.group("specifier")
msg = f"Setting collection specifier: {collection.specifier}"
logger.debug(msg)

collection.local = False
msg = "Setting request as non-local"
logger.debug(msg)

return collection


def get_galaxy(collection: Collection) -> None:
"""Retrieve the collection name from the galaxy.yml file.
Args:
collection: A collection object
Raises:
SystemExit: If the collection name is not found
"""
if collection is None or collection.path is None:
msg = "get_galaxy called without a collection or path"
raise RuntimeError(msg)
file_name = collection.path / "galaxy.yml"
if not file_name.exists():
err = f"Failed to find {file_name} in {collection.path}"
logger.critical(err)

with file_name.open(encoding="utf-8") as fileh:
try:
yaml_file = yaml.safe_load(fileh)
except yaml.YAMLError as exc:
err = f"Failed to load yaml file: {exc}"
logger.critical(err)

try:
collection.cnamespace = yaml_file["namespace"]
collection.cname = yaml_file["name"]
msg = f"Found collection name: {collection.name} from {file_name}."
logger.debug(msg)
except KeyError as exc:
err = f"Failed to find collection name in {file_name}: {exc}"
logger.critical(err)
else:
return
raise SystemExit(1) # We shouldn't be here
97 changes: 16 additions & 81 deletions src/pip4a/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@
from pathlib import Path
from typing import TYPE_CHECKING

import yaml

from .utils import parse_collection_request, subprocess_run
from .utils import subprocess_run


if TYPE_CHECKING:
from argparse import Namespace

from .utils import CollectionSpec, TermFeatures
from .utils import TermFeatures

logger = logging.getLogger(__name__)

Expand All @@ -40,23 +38,12 @@ def __init__(
self.python_path: Path
self.site_pkg_path: Path
self.venv_interpreter: Path
self.collection: CollectionSpec
self.term_features: TermFeatures = term_features

def init(self: Config) -> None:
"""Initialize the configuration."""
if self.args.venv:
self._create_venv = True
if self.args.subcommand == "install" and not self.args.requirement:
self.collection = parse_collection_request(self.args.collection_specifier)
if self.collection.path:
self._get_galaxy()

elif self.args.subcommand == "uninstall" and not self.args.requirement:
self.collection = parse_collection_request(self.args.collection_specifier)
if self.collection.path:
err = "Please use a collection name for uninstallation."
logger.critical(err)

self._set_interpreter()
self._set_site_pkg_path()
Expand Down Expand Up @@ -86,22 +73,6 @@ def venv_cache_dir(self: Config) -> Path:
"""Return the virtual environment cache directory."""
return self.cache_dir

@property
def collection_cache_dir(self: Config) -> Path:
"""Return the collection cache directory."""
collection_cache_dir = self.venv_cache_dir / self.collection.name
if not collection_cache_dir.exists():
collection_cache_dir.mkdir()
return collection_cache_dir

@property
def collection_build_dir(self: Config) -> Path:
"""Return the collection cache directory."""
collection_build_dir = self.collection_cache_dir / "build"
if not collection_build_dir.exists():
collection_build_dir.mkdir()
return collection_build_dir

@property
def discovered_python_reqs(self: Config) -> Path:
"""Return the discovered python requirements file."""
Expand All @@ -120,18 +91,6 @@ def site_pkg_collections_path(self: Config) -> Path:
site_pkg_collections_path.mkdir()
return site_pkg_collections_path

@property
def site_pkg_collection_path(self: Config) -> Path:
"""Return the site packages collection path."""
if not self.collection.cnamespace or not self.collection.cname:
msg = "Collection namespace or name not set."
raise RuntimeError(msg)
return (
self.site_pkg_collections_path
/ self.collection.cnamespace
/ self.collection.cname
)

@property
def venv_bindir(self: Config) -> Path:
"""Return the virtual environment bin directory."""
Expand All @@ -142,42 +101,6 @@ def interpreter(self: Config) -> Path:
"""Return the current interpreter."""
return Path(sys.executable)

def _get_galaxy(self: Config) -> None:
"""Retrieve the collection name from the galaxy.yml file.
Returns:
str: The collection name and dependencies
Raises:
SystemExit: If the collection name is not found
"""
if self.collection is None or self.collection.path is None:
msg = "_get_galaxy called without a collection or path"
raise RuntimeError(msg)
file_name = self.collection.path / "galaxy.yml"
if not file_name.exists():
err = f"Failed to find {file_name} in {self.collection.path}"
logger.critical(err)

with file_name.open(encoding="utf-8") as fileh:
try:
yaml_file = yaml.safe_load(fileh)
except yaml.YAMLError as exc:
err = f"Failed to load yaml file: {exc}"
logger.critical(err)

try:
self.collection.cnamespace = yaml_file["namespace"]
self.collection.cname = yaml_file["name"]
msg = f"Found collection name: {self.collection.name} from {file_name}."
logger.debug(msg)
except KeyError as exc:
err = f"Failed to find collection name in {file_name}: {exc}"
logger.critical(err)
else:
return
raise SystemExit(1) # We shouldn't be here

def _set_interpreter(
self: Config,
) -> None:
Expand All @@ -187,8 +110,14 @@ def _set_interpreter(
msg = f"Creating virtual environment: {self.venv}"
logger.debug(msg)
command = f"python -m venv {self.venv}"
work = "Creating virtual environment"
try:
subprocess_run(command=command, verbose=self.args.verbose)
subprocess_run(
command=command,
verbose=self.args.verbose,
msg=work,
term_features=self.term_features,
)
msg = f"Created virtual environment: {self.venv}"
logger.info(msg)
except subprocess.CalledProcessError as exc:
Expand All @@ -214,8 +143,14 @@ def _set_site_pkg_path(self: Config) -> None:
f"{self.venv_interpreter} -c"
" 'import json,site; print(json.dumps(site.getsitepackages()))'"
)
work = "Locating site packages directory"
try:
proc = subprocess_run(command=command, verbose=self.args.verbose)
proc = subprocess_run(
command=command,
verbose=self.args.verbose,
msg=work,
term_features=self.term_features,
)
except subprocess.CalledProcessError as exc:
err = f"Failed to find site packages path: {exc}"
logger.critical(err)
Expand Down
8 changes: 7 additions & 1 deletion src/pip4a/subcommands/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,15 @@ def _python_deps(self: Checker) -> None:
f" {self._config.discovered_python_reqs} --dry-run"
f" --report {missing_file}"
)
work = "Building python package dependency tree"

try:
subprocess_run(command=command, verbose=self._config.args.verbose)
subprocess_run(
command=command,
verbose=self._config.args.verbose,
msg=work,
term_features=self._config.term_features,
)
except subprocess.CalledProcessError as exc:
err = f"Failed to check python dependencies: {exc}"
logger.critical(err)
Expand Down
Loading

0 comments on commit 957373b

Please sign in to comment.