Compare commits

..

2 Commits

Author SHA1 Message Date
203c024287 move argument parsers to handlers themselves 2024-10-07 02:58:10 +03:00
22600a9eac chore: contributing guide update 2024-10-06 15:06:22 +03:00
10 changed files with 90 additions and 43 deletions

View File

@ -236,16 +236,50 @@ The projects also uses typing checks (provided by `mypy`) and some linter checks
tox
```
Must be usually done before any pushes.
### Generate documentation templates
```shell
tox -e docs
```
Must be usually done if there are changes in modules structure.
### Create release
```shell
tox -m release -- x.y.z
tox -m release -- major.minor.patch
```
The command above will generate documentation, tags, etc., and will push them to GitHub. Other things will be handled by GitHub workflows automatically.
### Hotfixes policy
Sometimes it is required to publish hotfix with specific commits, but some features have been already committed, which should not be included to the hotfix. In this case, some manual steps are required:
1. Create new branch from the last stable release (`major.minor.patch`):
```shell
git checkout -b release/major.minor major.minor.patch
```
2. Cherry-pick desired commit(s):
```shell
git cherry-pick <commit-sha>
```
Alternatively, make changes to the new branch and commit them.
3. Push newly created branch to remote:
```shell
git push --set-upstream origin release/major.minor
```
4. Proceed to release as usual:
```shell
tox -m release -- major.minor.patch+1
```

View File

@ -30,12 +30,14 @@ class ImportType(StrEnum):
import type enumeration
Attributes:
Future(MethodTypeOrder): (class attribute) from __future__ import
Package(MethodTypeOrder): (class attribute) package import
PackageFrom(MethodTypeOrder): (class attribute) package import, from clause
System(ImportType): (class attribute) system installed packages
SystemFrom(MethodTypeOrder): (class attribute) system installed packages, from clause
"""
Future = "future"
Package = "package"
PackageFrom = "package-from"
System = "system"
@ -70,6 +72,7 @@ class ImportOrder(BaseRawFileChecker):
"import-type-order",
{
"default": [
"future",
"system",
"system-from",
"package",
@ -91,7 +94,7 @@ class ImportOrder(BaseRawFileChecker):
)
@staticmethod
def imports(source: Iterable[Any], start_lineno: int = 0) -> list[nodes.Import | nodes.ImportFrom]:
def imports(source: Iterable[Any], start_lineno: int = 0) -> Iterable[nodes.Import | nodes.ImportFrom]:
"""
extract import nodes from list of raw nodes
@ -100,7 +103,7 @@ class ImportOrder(BaseRawFileChecker):
start_lineno(int, optional): minimal allowed line number (Default value = 0)
Returns:
list[nodes.Import | nodes.ImportFrom]: list of import nodes
Iterable[nodes.Import | nodes.ImportFrom]: list of import nodes
"""
def is_defined_import(imports: Any) -> bool:
@ -108,7 +111,7 @@ class ImportOrder(BaseRawFileChecker):
and imports.lineno is not None \
and imports.lineno >= start_lineno
return list(filter(is_defined_import, source))
return sorted(filter(is_defined_import, source), key=lambda imports: imports.lineno)
def check_from_imports(self, imports: nodes.ImportFrom) -> None:
"""
@ -124,30 +127,36 @@ class ImportOrder(BaseRawFileChecker):
self.add_message("from-imports-out-of-order", line=imports.lineno, args=(real, expected))
break
def check_imports(self, imports: list[nodes.Import | nodes.ImportFrom], root_package: str) -> None:
def check_imports(self, imports: Iterable[nodes.Import | nodes.ImportFrom], root_package: str) -> None:
"""
check imports
Args:
imports(list[nodes.Import | nodes.ImportFrom]): list of imports in their defined order
imports(Iterable[nodes.Import | nodes.ImportFrom]): list of imports in their defined order
root_package(str): root package name
"""
last_statement: tuple[int, str] | None = None
for statement in imports:
# define types and perform specific checks
if isinstance(statement, nodes.ImportFrom):
import_name = statement.modname
root, *_ = import_name.split(".", maxsplit=1)
import_type = ImportType.PackageFrom if root_package == root else ImportType.SystemFrom
# check from import itself
self.check_from_imports(statement)
else:
import_name = next(name for name, _ in statement.names)
root, *_ = import_name.split(".", maxsplit=1)[0]
import_type = ImportType.Package if root_package == root else ImportType.System
# check import itself
self.check_package_imports(statement)
match statement:
case nodes.ImportFrom() if statement.modname == "__future__":
import_name = statement.modname
import_type = ImportType.Future
case nodes.ImportFrom():
import_name = statement.modname
root, *_ = import_name.split(".", maxsplit=1)
import_type = ImportType.PackageFrom if root_package == root else ImportType.SystemFrom
# check from import itself
self.check_from_imports(statement)
case nodes.Import():
import_name = next(name for name, _ in statement.names)
root, *_ = import_name.split(".", maxsplit=1)
import_type = ImportType.Package if root_package == root else ImportType.System
# check import itself
self.check_package_imports(statement)
case _:
continue
# extract index
try:

View File

@ -21,6 +21,8 @@ import argparse
from pathlib import Path
import ahriman.application.handlers
from ahriman import __version__
from ahriman.application.handlers.handler import Handler
from ahriman.application.help_formatter import _HelpFormatter
@ -87,8 +89,7 @@ Start web service (requires additional configuration):
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command")
handlers_root = Path(__file__).parent / "handlers"
for handler in implementations(handlers_root, "ahriman.application.handlers", Handler):
for handler in implementations(ahriman.application.handlers, Handler):
for subparser_parser in handler.arguments:
subparser = subparser_parser(subparsers)
subparser.formatter_class = _HelpFormatter

View File

@ -17,14 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Any
try:
import aiohttp_security
_has_aiohttp_security = True
except ImportError:
_has_aiohttp_security = False
from typing import Any
__all__ = ["authorized_userid", "check_authorized", "forget", "remember"]

View File

@ -17,7 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from logging import NullHandler # pylint: disable=imports-out-of-order
# pylint: disable=imports-out-of-order
from logging import NullHandler
from typing import Any

View File

@ -23,6 +23,7 @@ from collections.abc import Generator
from importlib import import_module
from pathlib import Path
from pkgutil import ModuleInfo, walk_packages
from types import ModuleType
from typing import Any, TypeGuard, TypeVar
@ -51,13 +52,12 @@ def _modules(module_root: Path, prefix: str) -> Generator[ModuleInfo, None, None
yield module_info
def implementations(module_root: Path, prefix: str, base_class: type[T]) -> Generator[type[T], None, None]:
def implementations(root_module: ModuleType, base_class: type[T]) -> Generator[type[T], None, None]:
"""
extract implementations of the ``base_class`` from the module
Args:
module_root(Path): root module path with implementations
prefix(str): modules package prefix
root_module(ModuleType): root module
base_class(type[T]): base class type
Yields:
@ -68,7 +68,11 @@ def implementations(module_root: Path, prefix: str, base_class: type[T]) -> Gene
and issubclass(clazz, base_class) and clazz != base_class \
and clazz.__module__ == module.__name__
for module_info in _modules(module_root, prefix):
module = import_module(module_info.name)
for _, attribute in inspect.getmembers(module, is_base_class):
yield attribute
prefix = root_module.__name__
for module_root in root_module.__path__:
for module_info in _modules(Path(module_root), prefix):
module = import_module(module_info.name)
for _, attribute in inspect.getmembers(module, is_base_class):
yield attribute

View File

@ -18,7 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import Application, View
from pathlib import Path
import ahriman.web.views
from ahriman.core.configuration import Configuration
from ahriman.core.module_loader import implementations
@ -28,19 +29,18 @@ from ahriman.web.views.base import BaseView
__all__ = ["setup_routes"]
def _dynamic_routes(module_root: Path, configuration: Configuration) -> dict[str, type[View]]:
def _dynamic_routes(configuration: Configuration) -> dict[str, type[View]]:
"""
extract dynamic routes based on views
Args:
module_root(Path): root module path with views
configuration(Configuration): configuration instance
Returns:
dict[str, type[View]]: map of the route to its view
"""
routes: dict[str, type[View]] = {}
for view in implementations(module_root, "ahriman.web.views", BaseView):
for view in implementations(ahriman.web.views, BaseView):
view_routes = view.routes(configuration)
routes.update([(route, view) for route in view_routes])
@ -57,6 +57,5 @@ def setup_routes(application: Application, configuration: Configuration) -> None
"""
application.router.add_static("/static", configuration.getpath("web", "static_path"), follow_symlinks=True)
views_root = Path(__file__).parent / "views"
for route, view in _dynamic_routes(views_root, configuration).items():
for route, view in _dynamic_routes(configuration).items():
application.router.add_view(route, view)

View File

@ -1,15 +1,16 @@
import ahriman.web.views
from pathlib import Path
from ahriman.core.module_loader import _modules, implementations
from ahriman.web.views.base import BaseView
def test_implementations(resource_path_root: Path) -> None:
def test_implementations() -> None:
"""
must load implementations from the package
"""
views_root = resource_path_root / ".." / ".." / "src" / "ahriman" / "web" / "views"
routes = list(implementations(views_root, "ahriman.web.views", BaseView))
routes = list(implementations(ahriman.web.views, BaseView))
assert routes
assert all(isinstance(view, type) for view in routes)
assert all(issubclass(view, BaseView) for view in routes)

View File

@ -17,7 +17,7 @@ def test_dynamic_routes(resource_path_root: Path, configuration: Configuration)
if file.suffix == ".py" and file.name not in ("__init__.py", "base.py", "status_view_guard.py")
]
routes = _dynamic_routes(views_root, configuration)
routes = _dynamic_routes(configuration)
assert all(isinstance(view, type) for view in routes.values())
assert len(set(routes.values())) == len(expected_views)

View File

@ -27,8 +27,6 @@ description = Run common checks like linter, mypy, etc
deps =
{[tox]dependencies}
-e .[check]
allowlist_externals =
bash
setenv =
MYPYPATH=src
commands =
@ -36,7 +34,7 @@ commands =
pylint --rcfile=.pylintrc "src/{[tox]project_name}"
bandit -c .bandit.yml -r "src/{[tox]project_name}"
bandit -c .bandit-test.yml -r "tests/{[tox]project_name}"
bash -c 'mypy {[mypy]flags} -p "{[tox]project_name}" --install-types --non-interactive || mypy {[mypy]flags} -p "{[tox]project_name}"'
mypy {[mypy]flags} -p "{[tox]project_name}" --install-types --non-interactive
[testenv:docs]
description = Generate source files for documentation