Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup DIG #34

Merged
merged 18 commits into from
Dec 20, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
New: initialize new DIG for #7 (@wip)
  • Loading branch information
KyleKing committed Dec 18, 2020
commit 257d4c1a29bf411e9d41b8adda4092669a3029a1
4 changes: 2 additions & 2 deletions calcipy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
"""Loguru configuration. Loguru is deactivated for this package by default and must be activated.

```py
from this_package import __pkg__name__
from this_package_name_here import LOGGER_CONFIG, __pkg__name__

logger.configure(**LOGGER_CONFIG)
logger.enable(__pkg__name__)

# You can continue to import and enable additional packages as needed, but you should only call configure once
# You can continue to import and enable additional packages as needed, but you should only call 'configure' once
```

"""
Expand Down
6 changes: 3 additions & 3 deletions calcipy/doit_helpers/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ def task_tag_remove() -> DoItTask:
\"\"\"Loguru configuration. Loguru is deactivated for this package by default and must be activated.

```py
from this_package import __pkg__name__
from this_package_name_here import LOGGER_CONFIG, __pkg__name__

logger.configure(**LOGGER_CONFIG)
logger.enable(__pkg__name__)

# You can continue to import and enable additional packages as needed, but you should only call configure once
# You can continue to import and enable additional packages as needed, but you should only call 'configure' once
```

\"\"\"
Expand Down Expand Up @@ -259,7 +259,7 @@ def task_document() -> DoItTask:
(_write_code_to_readme, ()),
(_write_coverage_to_readme, ()),
(_write_readme_to_init, ()),
# args = f'{DIG.pkg_name} --html --force --template-dir "{DIG.template_dir}" --output-dir "{DIG.doc_dir}"'
# args = f'{DIG.pkg_name} --html --force --output-dir "{DIG.doc_dir}"'
# f'poetry run portray {args}', # PLANNED: Implement portray or mkdocs!
])

Expand Down
249 changes: 248 additions & 1 deletion calcipy/doit_helpers/doit_globals.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Global Variables for DoIt."""

import inspect
import warnings
from functools import partial
from pathlib import Path
from typing import Callable, Dict, NewType, Optional, Sequence, Tuple, Union
from typing import Any, Callable, Dict, List, NewType, Optional, Sequence, Tuple, Union

import attr
import toml
from loguru import logger

Expand All @@ -15,5 +19,248 @@
"""DoIt task type for annotations."""


def _member_filter(member: Any, instance_type: Any, prefix: Optional[str],
debug: bool = False) -> bool:
"""Return True if the member matches the filters.

Args:
cls: class
instance_type: optional instance type
prefix: optional string prefix to check starts with
debug: if True, will log debug information

Returns:
List[Tuple[str, Callable]]: filtered members from the class

"""
match_instance = (instance_type is None or isinstance(member, instance_type))
match_prefix = (prefix is None or (hasattr(member, '__name__') # noqa: P002
and member.__name__.startswith(prefix)))
if debug:
name = member.__name__ if hasattr(member, '__name__') else '__no_name__' # noqa: P002
logger.debug('{match_instance} and {match_prefix} ({name}={member})', match_instance=match_instance,
match_prefix=match_prefix, member=member, name=name)
return match_instance and match_prefix


def _get_members(cls: object, **kwargs: Any) -> List[Tuple[str, Callable]]:
"""Return the members that match the parameters.

Example to return all methods that start with `do_`: `_get_members(cls, instance_type=Callable, prefix='do_')`

Args:
cls: class
**kwargs: keyword arguments passed to `_member_filter`

Returns:
List[Tuple[str, Callable]]: filtered members from the class

"""
return inspect.getmembers(cls, predicate=partial(_member_filter, **kwargs))


def _verify_initialized_paths(cls: object) -> None:
"""Verify that all paths are not None.

WARN: will not raise on error the class attribute

Args:
cls: class

Raises:
RuntimeError: if any paths are None

"""
logger.info(f'Class: {cls}')

missing = []
for name, path_raw in _get_members(cls, instance_type=(type(Path()), type(None)), prefix=None):
if name.startswith('path_') and path_raw is None:
# ^ PLANNED: filter for startswith in `_get_members` instead of `_member_filter`
missing.append(name)
else:
logger.warning(f'{name}, {path_raw}, {type(path_raw)}')

if missing:
kwargs = ', '.join(missing)
raise RuntimeError(f'Missing keyword arguments for: {kwargs}')


def _resolve_class_paths(cls: object, base_path: Path) -> None:
"""Resolve all partial paths with the specified base path.

WARN: will mutate the class attribute

Args:
cls: class
base_path: base path to apply to all found relative paths

"""
logger.info(f'Class: {cls}')
for name, path_raw in _get_members(cls, instance_type=type(Path()), prefix=None):
logger.debug(f'self.{name}={path_raw} ({path_raw.is_absolute()})')
if not path_raw.is_absolute():
setattr(cls, name, base_path / path_raw)
logger.info(f'Mutated: self.{name}={path_raw} (now: {getattr(cls, name)})')


_DEF_EXCLUDE = [*map(Path, ['__init__.py'])]
"""Default list of excluded filenames."""


@attr.s(auto_attribs=True, kw_only=True)
class _PathAttrBase: # noqa: H601

path_source: Path
"""Path to the package directory."""

def __attrs_post_init__(self) -> None:
"""Initialize full paths with the package base directory if necessary.

Raises:
RuntimeError: if any paths are None

"""
if self.path_source is None:
raise RuntimeError('Missing keyword argument "path_source"')
_resolve_class_paths(self, self.path_source)
_verify_initialized_paths(self)


@attr.s(auto_attribs=True, kw_only=True)
class PackageMeta(_PathAttrBase): # noqa: H601
"""Package Meta-Information."""

path_toml: Path = Path('pyproject.toml')
"""Path to the poetry toml file."""

pkg_name: str = attr.ib(init=False)
"""Package string name."""

pkg_version: str = attr.ib(init=False)
"""Package version."""

def __attrs_post_init__(self) -> None:
"""Finish initializing class attributes.

Raises:
FileNotFoundError: if the toml could not be located

"""
super().__attrs_post_init__()
try:
poetry_config = toml.load(self.path_toml)['tool']['poetry']
except FileNotFoundError:
raise FileNotFoundError(f'Check that "{self.path_source}" is correct. Could not find: {self.path_toml}')

self.pkg_name = poetry_config['name']
self.pkg_version = poetry_config['version']

if '-' in self.pkg_name:
warnings.warn(f'Replace dashes in name with underscores ({self.pkg_name}) in {self.path_toml}')


@attr.s(auto_attribs=True, kw_only=True)
class LintConfig(_PathAttrBase): # noqa: H601
"""Lint Config."""

path_flake8: Path = Path('.flake8')
"""Path to the flake8 configuration file."""

paths: List[Path] = []
"""List of file and directory Paths to lint."""

paths_excluded: List[Path] = _DEF_EXCLUDE
"""List of excluded relative Paths."""


@attr.s(auto_attribs=True, kw_only=True)
class TestConfig(_PathAttrBase): # noqa: H601
"""Test Config."""

path_out: Path
"""Path to the report output directory."""

path_test_dir: Path = Path('tests')
"""Path to the tests directory."""

path_report_index: Path = attr.ib(init=False)
"""Path to the report HTML file."""

path_coverage_index: Path = attr.ib(init=False)
"""Path to the coverage HTML file."""

def __attrs_post_init__(self) -> None:
"""Finish initializing class attributes."""
super().__attrs_post_init__()
self.path_report_index = self.path_out / 'test_report.html'
self.path_coverage_index = self.path_out / 'cov_html/index.html'


@attr.s(auto_attribs=True, kw_only=True)
class DocConfig(_PathAttrBase): # noqa: H601
"""Documentation Config."""

path_out: Path
"""Path to the documentation output directory."""

path_changelog: Path = Path('.gitchangelog.rc')
"""Path to the changelog configuration file."""

paths_excluded: List[Path] = _DEF_EXCLUDE
"""List of excluded relative Paths."""


@attr.s(auto_attribs=True, kw_only=True)
class DoItGlobals:
"""Global Variables for DoIt."""

calcipy_dir: Path = Path(__file__).parents[1]
"""The calcipy directory (likely within `.venv`)."""

meta: PackageMeta = attr.ib(init=False) # PLANNED: Check if Optional[PackageMeta] is necessary
"""Package Meta-Information."""

lint_config: LintConfig = attr.ib(init=False)
"""Lint Config."""

test_config: TestConfig = attr.ib(init=False)
"""Test Config."""

doc_config: DocConfig = attr.ib(init=False)
"""Documentation Config."""

@log_fun
def set_paths(self, *, path_source: Optional[Path] = None,
doc_dir: Optional[Path] = None) -> None:
"""Set data members based on working directory.

Args:
path_source: optional source directory Path. Defaults to the `pkg_name`
doc_dir: optional destination directory for project documentation. Defaults to './output'

"""
logger.info(f'Setting DIG paths for {path_source}', path_source=path_source, cwd=Path.cwd(), doc_dir=doc_dir)
path_source = Path.cwd() if path_source is None else path_source
self.meta = PackageMeta(path_source=path_source)
meta_kwargs = {'path_source': self.meta.path_source}

self.lint_config = LintConfig(**meta_kwargs)
self.lint_config.paths.append(self.meta.path_source / self.meta.pkg_name)

self.test_config = TestConfig(path_out=Path(), **meta_kwargs)

doc_dir = self.meta.path_source / 'docs' if doc_dir is None else doc_dir
self.doc_config = DocConfig(path_out=doc_dir, **meta_kwargs)

logger.info(self)


DIG = DoItGlobals()
"""Global DoIt Globals class used to manage global variables."""

# HACK: Temporary for debugging
logger.enable('calcipy')
logger.debug(DIG)

DIG.set_paths()
2 changes: 1 addition & 1 deletion calcipy/doit_helpers/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def _collect_py_files(add_paths: Sequence[Path] = (), sub_directories: Optional[
if not isinstance(add_paths, (list, tuple)):
raise TypeError(f'Expected add_paths to be a list of Paths, but received: {add_paths}')
if sub_directories is None:
sub_directories = [DIG.pkg_name] + DIG.external_doc_dirs
sub_directories = [DIG.pkg_name] # FIXME: maybe `+ DIG.lint_config.paths`?
package_files = [*add_paths] + [*DIG.source_path.glob('*.py')]
for subdir in sub_directories: # Capture files in package and in tests directory
package_files.extend([*(DIG.source_path / subdir).rglob('*.py')])
Expand Down
2 changes: 1 addition & 1 deletion calcipy/log_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,5 @@ def log_fun(fun: Callable, *args: Any, **kwargs: Any) -> Any:

"""
fun_name = fun.__name__
with log_action(f'Entering {fun_name}{signature(fun)}', args=args, kwargs=kwargs):
with log_action(f'Running {fun_name}{signature(fun)}', args=args, kwargs=kwargs):
return fun(*args, **kwargs)
2 changes: 1 addition & 1 deletion tests/test_doit_helpers/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_dig_props():
"""Test the DIG global variable from DoItGlobals."""
public_props = ['coverage_path', 'calcipy_dir', 'doc_dir', 'excluded_files', 'external_doc_dirs', 'flake8_path',
'lint_paths', 'path_gitchangelog', 'pkg_name', 'set_paths', 'source_path', 'src_examples_dir',
'test_path', 'test_report_path', 'tmp_examples_dir', 'toml_path', 'template_dir', 'pkg_version']
'test_path', 'test_report_path', 'tmp_examples_dir', 'toml_path', 'pkg_version']
dig = DoItGlobals()

result = [prop for prop in dir(dig) if not prop.startswith('_')]
Expand Down
26 changes: 26 additions & 0 deletions tests/test_doit_helpers/test_doit_globals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Test doit_helpers/doit_globals.py."""

from pathlib import Path

import pytest

from calcipy.doit_helpers.doit_globals import DocConfig, TestConfig


def test_path_attr_base_path_resolver():
"""Test the _PathAttrBase class."""
base_path = Path().resolve()

dp = DocConfig(path_source=base_path) # act

assert not DocConfig.path_changelog.is_absolute()
assert dp.path_changelog.is_absolute()
assert dp.path_changelog == base_path / DocConfig.path_changelog


# Parametrize for path_source to be none or a path and show that both raise an error for different missing paths...
# >> RuntimeError: Missing keyword arguments for: path_out
# >> RuntimeError: Missing keyword arguments for: path_out, path_source
# def test_path_attr_base_path_verifier():
# with pytest.raises(RuntimeError, tbd=''):
# TestConfig(path_source=None)