Skip to content

Commit

Permalink
package name to valid dotted path
Browse files Browse the repository at this point in the history
- fix: package name to valid dotted path before package resource extraction (#3)
- chore(pre-commit): add mypy check
  • Loading branch information
msftcangoblowm committed Dec 30, 2024
1 parent 280a984 commit fe347f6
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 105 deletions.
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ repos:
hooks:
- id: isort

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.1
hooks:
- id: mypy
pass_filenames: false
entry: mypy src/logging_strict/

- repo: local
hooks:
- id: validate-logging-config-yaml
Expand Down
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ Changelog
Known regressions
..................

- LoggingConfigYaml.extract if package name is not a valid dotted path
e.g. logging-strict bombs and is hard to spot. Create an Issue then a commit

- strictyaml.scalar.Time does not exist. So field asTime can't be supported
- strictyaml has no automated tests
- strictyaml has no typing hint stubs. ignore_missing_imports
Expand All @@ -48,6 +51,9 @@ Changelog
Commit items for NEXT VERSION
..............................

- fix: package name to valid dotted path before package resource extraction (#3)
- chore(pre-commit): add mypy check

.. scriv-start-here
.. _changes_1-4-1:
Expand Down
42 changes: 34 additions & 8 deletions src/logging_strict/logging_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
PackageResource,
PartStem,
PartSuffix,
_to_package_case,
filter_by_file_stem,
filter_by_suffix,
)
Expand Down Expand Up @@ -230,13 +231,8 @@ def __init__(
"""Class constructor"""
super().__init__()

if is_ok(package_name):
self._package_name = package_name
else:
msg_exc = (
"Package name required. Which package contains logging.config files?"
)
raise LoggingStrictPackageNameRequired(msg_exc)
# may raise LoggingStrictPackageNameRequired
self.package = package_name

if is_ok(package_data_folder_start):
self._package_data_folder_start = package_data_folder_start
Expand Down Expand Up @@ -457,6 +453,35 @@ def package(self):
"""
return self._package_name

@package.setter
def package(self, val):
"""Package name is supposed to be a dotted path.
Apply :py:func:`logging_strict.util.package_resource._to_package_case`
:param val: package name must be a dotted path
:type val: typing.Any
:raises:
- :py:exc:`LoggingStrictPackageNameRequired` -- Package name
required for determining destination folder
"""
if is_ok(val):
"""Expects a valid package name; a dotted path. Coerse into
a dotted path.
e.g. logging-strict --> logging_strict
Understandable mistake. Very hard to spot. Nearly correct.
"""
self._package_name = _to_package_case(val)
else:
msg_exc = (
"Package name required. Which package contains logging.config files?"
)
raise LoggingStrictPackageNameRequired(msg_exc)

@property
def dest_folder(self):
"""Normally xdg user data dir. During testing, temp folder used instead
Expand Down Expand Up @@ -516,7 +541,8 @@ def extract(self, path_relative_package_dir=""):
file_suffix = self.file_suffix
file_name = f"??.{file_suffix}" if file_stem is None else self.file_name

pr = PackageResource(self.package, self._package_data_folder_start)
package_name_dotted_path = self.package
pr = PackageResource(package_name_dotted_path, self._package_data_folder_start)

try:
gen = pr.package_data_folders(
Expand Down
2 changes: 2 additions & 0 deletions src/logging_strict/logging_api.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class LoggingConfigYaml(LoggingYamlType):
def file_name(self) -> str: ...
@property
def package(self) -> str: ...
@package.setter
def package(self, val: Any) -> None: ...
@property
def dest_folder(self) -> Path: ...
def extract(
Expand Down
19 changes: 1 addition & 18 deletions src/logging_strict/logging_yaml_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
import abc
import glob
import logging.config
import re
from pathlib import (
Path,
PurePath,
Expand All @@ -67,6 +66,7 @@
is_not_ok,
is_ok,
)
from .util.package_resource import _to_package_case
from .util.xdg_folder import _get_path_config

__all__ = (
Expand All @@ -81,23 +81,6 @@
PACKAGE_NAME_SRC = "package_name"


def _to_package_case(val):
"""Takes lowercase and converts non-alphanumeric characters to underscore
:param val:
An arbitrary str. Expected non-alphanumeric chars hyphen
period and underscore. Although will convert all non-alphanumeric chars
:type val: str
:returns: valid package name containing only alphanumeric and underscore chars
:rtype: str
"""
ret = re.sub("[^a-z0-9]+", "_", val.lower())

return ret


def _update_logger_package_name(
d_config,
package_name=None,
Expand Down
1 change: 0 additions & 1 deletion src/logging_strict/logging_yaml_abc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ PATTERN_DEFAULT: Final[str]
VERSION_FALLBACK: str
PACKAGE_NAME_SRC: str

def _to_package_case(val: str) -> str: ...
def _update_logger_package_name(
d_config: dict[str, Any],
package_name: str | None = None,
Expand Down
45 changes: 44 additions & 1 deletion src/logging_strict/util/package_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@

import importlib.resources as importlib_resources # py39+
import logging
import re
import shutil
import sys
import traceback
Expand Down Expand Up @@ -654,9 +655,50 @@ def is_package_exists(package_name):
return ret


def _to_package_case(val):
"""Sanitize package name to a valid dotted path
The ultimate test is
:py:func:`logging_strict.util.package_resource._get_package_data_folder`.
Which wraps :py:func:`importlib_resources.files`. Expects a dotted path.
If :py:func:`importlib_resources.files` doesn't get a valid dotted path,
returns None. Which can be unexpected.
Acts as a mitigation fix to allow for understandable simple human
errors.
:param val:
An arbitrary str. Unallowed chars will be converted into hyphens
:type val: str
:returns:
Valid package name can contain: alphanumeric, underscore, or period chars.
A period denotes a namespace package
:rtype: str
"""
ret = re.sub("[^a-z0-9.]+", "_", val.lower())

return ret


def _get_package_data_folder(dotted_path):
"""Helper that retrieves the package resource
If :py:func:`importlib_resources.files` doesn't get a valid dotted path,
returns None. Which can be unexpected.
Better UX would allow for understandable simple human errors.
Mitigate by fixing weird characters --> underscore. While allowing
namespace packages (e.g. ``zope.interface``).
With the mitigation fix, None means the package is not installed
rather than a hard to track down typo.
:param dotted_path: package_name and optionally dotted path to a subfolder
:type dotted_path: str
:returns:
Expand All @@ -665,8 +707,9 @@ def _get_package_data_folder(dotted_path):
:rtype: importlib.resources.abc.Traversable | None
"""
dotted_path_valid = _to_package_case(dotted_path)
try:
trav_ret = importlib_resources.files(dotted_path)
trav_ret = importlib_resources.files(dotted_path_valid)
except ModuleNotFoundError:
# There is no such package or data folder
trav_ret = None
Expand Down
54 changes: 27 additions & 27 deletions src/logging_strict/util/package_resource.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,34 @@ is_module_debug: bool
g_module: str
_LOGGER: logging.Logger

def _extract_folder(package: str) -> str: ...
def _get_package_data_folder(dotted_path: str) -> Traversable | None: ...
def _to_package_case(val: str) -> str: ...
def check_folder(
x: Traversable,
cb_suffix: Callable[[str], bool] | None = None,
cb_file_stem: Callable[[str], bool] | None = None,
) -> Iterator[Traversable]: ...
def filter_by_file_stem(
expected_file_name: str,
test_file_name: str,
) -> bool: ...
def filter_by_suffix(
expected_suffix: str | tuple[str, ...],
test_suffix: str,
) -> bool: ...
def is_package_exists(package_name: str) -> bool: ...
def match_file(
y: Traversable,
/,
*,
cb_suffix: Callable[[str], bool] | None = None,
cb_file_stem: Callable[[str], bool] | None = None,
) -> bool: ...
def msg_stem(file_name: str) -> str: ...
def walk_tree_folders(
traversable_root: Traversable,
) -> Iterator[Traversable]: ...
@runtime_checkable
class PartSuffix(Protocol):
def __call__(
Expand All @@ -39,33 +66,6 @@ class PartStem(Protocol):
test_file_stem: str,
) -> bool: ...

def match_file(
y: Traversable,
/,
*,
cb_suffix: Callable[[str], bool] | None = None,
cb_file_stem: Callable[[str], bool] | None = None,
) -> bool: ...
def check_folder(
x: Traversable,
cb_suffix: Callable[[str], bool] | None = None,
cb_file_stem: Callable[[str], bool] | None = None,
) -> Iterator[Traversable]: ...
def filter_by_suffix(
expected_suffix: str | tuple[str, ...],
test_suffix: str,
) -> bool: ...
def filter_by_file_stem(
expected_file_name: str,
test_file_name: str,
) -> bool: ...
def _extract_folder(package: str) -> str: ...
def walk_tree_folders(
traversable_root: Traversable,
) -> Iterator[Traversable]: ...
def is_package_exists(package_name: str) -> bool: ...
def _get_package_data_folder(dotted_path: str) -> Traversable | None: ...

class PackageResource:
def __init__(
self,
Expand Down
15 changes: 1 addition & 14 deletions tests/test_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
from logging_strict.logging_yaml_abc import (
PACKAGE_NAME_SRC,
VERSION_FALLBACK,
_to_package_case,
_update_logger_package_name,
after_as_str_update_package_name,
)
Expand All @@ -43,6 +42,7 @@
get_locals,
is_class_attrib_kind,
)
from logging_strict.util.package_resource import _to_package_case

if sys.version_info >= (3, 9): # pragma: no cover
from collections.abc import Iterator
Expand Down Expand Up @@ -538,19 +538,6 @@ def test_after_as_str_update_package_name(self):
self.assertEqual(str_yaml_0, str_yaml_2)


class SanitizePackageName(unittest.TestCase):
"""Sanitizes package name.
Take lowercase then turns non-alphanumeric char into underscore"""

def test_to_package_case(self):
"""Sanitize package name"""
t_package_names = (("dog-food_yum.py", "dog_food_yum_py"),)
for package_name, expected in t_package_names:
actual = _to_package_case(package_name)
self.assertEqual(actual, expected)


if __name__ == "__main__": # pragma: no cover
"""Without coverage
Expand Down
Loading

0 comments on commit fe347f6

Please sign in to comment.