mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-09-01 14:29:55 +00:00
Compare commits
2 Commits
b36bcb194b
...
203c024287
Author | SHA1 | Date | |
---|---|---|---|
203c024287 | |||
22600a9eac |
@ -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
|
||||
```
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
4
tox.ini
4
tox.ini
@ -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
|
||||
|
Reference in New Issue
Block a user