Skip to content

Commit

Permalink
Resolve import cycles in manage module and its submodules
Browse files Browse the repository at this point in the history
  • Loading branch information
unkcpz committed Nov 15, 2024
1 parent 098dcb0 commit d25c096
Show file tree
Hide file tree
Showing 14 changed files with 486 additions and 485 deletions.
6 changes: 1 addition & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,7 @@ repos:
language: system
types: [python]
require_serial: true
args:
[
"--disable=all",
"--enable=cyclic-import",
]
args: [--disable=all, --enable=cyclic-import]

- id: dm-generate-all
name: Update all requirements files
Expand Down
4 changes: 2 additions & 2 deletions docs/source/topics/processes/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Notice how the order in which we pass the arguments is irrelevant because we spe
Now that we know the difference between positional and named arguments, it is important to realize a python requirement that **positional arguments have to come before named arguments**.
What this means is that *both* the function definition and function call below are illegal, because there are named arguments before positional ones:

.. include:: include/snippets/functions/signature_plain_python_call_illegal.py
.. include:: include/snippets/functions/signature_plain_python_call_illegal.illpy
:code: python

Finally, python knows the concept of ``*args`` and ``**kwargs``, which allow one to define a function that accepts a variable number of positional and keyword arguments (also known as a _variadic_ function).
Expand Down Expand Up @@ -190,7 +190,7 @@ This particularly useful when exposing a process function in a wrapping workchai
The user can now access the input description directly through the spec of the work chain, without having to go to the process function itself.
For example, in an interactive shell:

.. include:: include/snippets/functions/parse_docstring_expose_ipython.py
.. include:: include/snippets/functions/parse_docstring_expose_ipython.illpy
:code: ipython

Return values
Expand Down
8 changes: 7 additions & 1 deletion src/aiida/cmdline/commands/cmd_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from __future__ import annotations

from typing import TYPE_CHECKING

import click

from aiida.cmdline.commands.cmd_verdi import verdi
Expand All @@ -18,7 +20,10 @@
from aiida.cmdline.params.options.commands import setup
from aiida.cmdline.utils import defaults, echo
from aiida.common import exceptions
from aiida.manage.configuration import Profile, create_profile, get_config
from aiida.manage.configuration.config import get_config

if TYPE_CHECKING:
from aiida.manage.configuration import Profile


@verdi.group('profile')
Expand Down Expand Up @@ -56,6 +61,7 @@ def command_create_profile(
"""
from aiida.brokers.rabbitmq.defaults import detect_rabbitmq_config
from aiida.common import docs
from aiida.manage.configuration import create_profile
from aiida.plugins.entry_point import get_entry_point_from_class

if not storage_cls.read_only and email is None:
Expand Down
6 changes: 0 additions & 6 deletions src/aiida/manage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@
.. note:: Modules in this sub package may require the database environment to be loaded
"""

# AUTO-GENERATED

# fmt: off

from .caching import *
from .configuration import *
from .external import *
Expand All @@ -47,4 +42,3 @@
'upgrade_config',
)

# fmt: on
292 changes: 5 additions & 287 deletions src/aiida/manage/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@

from __future__ import annotations

# AUTO-GENERATED
# fmt: off
from .config import *
from .migrations import *
from .options import *
from .profile import *
Expand All @@ -31,294 +30,13 @@
'get_option_names',
'parse_option',
'upgrade_config',
)

# fmt: on

# END AUTO-GENERATED


__all__ += (
'create_profile',
'get_profile',
'load_profile',
'profile_context',
'get_config',
'get_config_option',
'get_config_path',
'get_profile',
'load_profile',
'reset_config',
'CONFIG',
)

import os
import warnings
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Optional

from aiida.common.warnings import AiidaDeprecationWarning

if TYPE_CHECKING:
from aiida.orm import User

from .config import Config

# global variables for aiida
CONFIG: Optional['Config'] = None


def get_config_path():
"""Returns path to .aiida configuration directory."""
from .settings import AIIDA_CONFIG_FOLDER, DEFAULT_CONFIG_FILE_NAME

return os.path.join(AIIDA_CONFIG_FOLDER, DEFAULT_CONFIG_FILE_NAME)


def load_config(create=False) -> 'Config':
"""Instantiate Config object representing an AiiDA configuration file.
Warning: Contrary to :func:`~aiida.manage.configuration.get_config`, this function is uncached and will always
create a new Config object. You may want to call :func:`~aiida.manage.configuration.get_config` instead.
:param create: if True, will create the configuration file if it does not already exist
:type create: bool
:return: the config
:rtype: :class:`~aiida.manage.configuration.config.Config`
:raises aiida.common.MissingConfigurationError: if the configuration file could not be found and create=False
"""
from aiida.common import exceptions

from .config import Config

filepath = get_config_path()

if not os.path.isfile(filepath) and not create:
raise exceptions.MissingConfigurationError(f'configuration file {filepath} does not exist')

try:
config = Config.from_file(filepath)
except ValueError as exc:
raise exceptions.ConfigurationError(f'configuration file {filepath} contains invalid JSON') from exc

_merge_deprecated_cache_yaml(config, filepath)

return config


def _merge_deprecated_cache_yaml(config, filepath):
"""Merge the deprecated cache_config.yml into the config."""
cache_path = os.path.join(os.path.dirname(filepath), 'cache_config.yml')
if not os.path.exists(cache_path):
return

# Imports are here to avoid them when not needed
import shutil

import yaml

from aiida.common import timezone

cache_path_backup = None
# Keep generating a new backup filename based on the current time until it does not exist
while not cache_path_backup or os.path.isfile(cache_path_backup):
cache_path_backup = f"{cache_path}.{timezone.now().strftime('%Y%m%d-%H%M%S.%f')}"

warnings.warn(
'cache_config.yml use is deprecated and support will be removed in `v3.0`. Merging into config.json and '
f'moving to: {cache_path_backup}',
AiidaDeprecationWarning,
stacklevel=2,
)

with open(cache_path, 'r', encoding='utf8') as handle:
cache_config = yaml.safe_load(handle)
for profile_name, data in cache_config.items():
if profile_name not in config.profile_names:
warnings.warn(f"Profile '{profile_name}' from cache_config.yml not in config.json, skipping", UserWarning)
continue
for key, option_name in [
('default', 'caching.default_enabled'),
('enabled', 'caching.enabled_for'),
('disabled', 'caching.disabled_for'),
]:
if key in data:
value = data[key]
# in case of empty key
value = [] if value is None and key != 'default' else value
config.set_option(option_name, value, scope=profile_name)
config.store()
shutil.move(cache_path, cache_path_backup)


def load_profile(profile: Optional[str] = None, allow_switch=False) -> 'Profile':
"""Load a global profile, unloading any previously loaded profile.
.. note:: if a profile is already loaded and no explicit profile is specified, nothing will be done
:param profile: the name of the profile to load, by default will use the one marked as default in the config
:param allow_switch: if True, will allow switching to a different profile when storage is already loaded
:return: the loaded `Profile` instance
:raises `aiida.common.exceptions.InvalidOperation`:
if another profile has already been loaded and allow_switch is False
"""
from aiida.manage import get_manager

return get_manager().load_profile(profile, allow_switch)


def get_profile() -> Optional['Profile']:
"""Return the currently loaded profile.
:return: the globally loaded `Profile` instance or `None`
"""
from aiida.manage import get_manager

return get_manager().get_profile()


@contextmanager
def profile_context(profile: 'Profile' | str | None = None, allow_switch=False) -> 'Profile':
"""Return a context manager for temporarily loading a profile, and unloading on exit.
:param profile: the name of the profile to load, by default will use the one marked as default in the config
:param allow_switch: if True, will allow switching to a different profile
:return: a context manager for temporarily loading a profile
"""
from aiida.manage import get_manager

manager = get_manager()
current_profile = manager.get_profile()
yield manager.load_profile(profile, allow_switch)
if current_profile is None:
manager.unload_profile()
else:
manager.load_profile(current_profile, allow_switch=True)


def create_default_user(
profile: Profile,
email: str,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
institution: Optional[str] = None,
) -> User:
"""Create a default user for the given profile.
If the profile's storage is read only, a random existing user will be queried and set as default. Otherwise a new
user is created with the provided details and set as user.
:param profile: The profile to create the user in.
:param email: Email for the default user.
:param first_name: First name for the default user.
:param last_name: Last name for the default user.
:param institution: Institution for the default user.
:returns: The user that was set as the default user.
"""
from aiida.manage import get_manager
from aiida.orm import User

with profile_context(profile, allow_switch=True):
manager = get_manager()
storage = manager.get_profile_storage()

if storage.read_only:
# Check if the storage contains any users, and just set a random one as default user.
user = User.collection.query().first(flat=True)
else:
# Otherwise create a user and store it
user = User(email=email, first_name=first_name, last_name=last_name, institution=institution).store()

# The user can be ``None`` if the storage is read-only and doesn't contain any users. This shouldn't happen in
# real situations, but this safe guard is added to be safe.
if user:
manager.set_default_user_email(profile, user.email)

return user


def create_profile(
config: 'Config',
*,
storage_backend: str,
storage_config: dict[str, Any],
broker_backend: str | None = None,
broker_config: dict[str, Any] | None = None,
name: str,
email: str,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
institution: Optional[str] = None,
is_test_profile: bool = False,
) -> Profile:
"""Create a new profile, initialise its storage and create a default user.
:param config: The config instance.
:param name: Name of the profile.
:param email: Email for the default user.
:param first_name: First name for the default user.
:param last_name: Last name for the default user.
:param institution: Institution for the default user.
:param create_user: If `True`, creates a user that is set as the default user.
:param storage_backend: The entry point to the :class:`aiida.orm.implementation.storage_backend.StorageBackend`
implementation to use for the storage.
:param storage_config: The configuration necessary to initialise and connect to the storage backend.
:param broker_backend: The entry point to the :class:`aiida.brokers.Broker` implementation to use for the broker.
:param broker_config: The configuration necessary to initialise and connect to the broker.
"""
profile: Profile = config.create_profile(
name=name,
storage_backend=storage_backend,
storage_config=storage_config,
broker_backend=broker_backend,
broker_config=broker_config,
is_test_profile=is_test_profile,
)

create_default_user(profile, email, first_name, last_name, institution)

return profile


def reset_config():
"""Reset the globally loaded config.
.. warning:: This is experimental functionality and should for now be used only internally. If the reset is unclean
weird unknown side-effects may occur that end up corrupting or destroying data.
"""
global CONFIG # noqa: PLW0603
CONFIG = None


def get_config(create=False):
"""Return the current configuration.
If the configuration has not been loaded yet
* the configuration is loaded using ``load_config``
* the global `CONFIG` variable is set
* the configuration object is returned
Note: This function will except if no configuration file can be found. Only call this function, if you need
information from the configuration file.
:param create: if True, will create the configuration file if it does not already exist
:type create: bool
:return: the config
:rtype: :class:`~aiida.manage.configuration.config.Config`
:raises aiida.common.ConfigurationError: if the configuration file could not be found, read or deserialized
"""
global CONFIG # noqa: PLW0603

if not CONFIG:
CONFIG = load_config(create=create)

if CONFIG.get_option('warnings.showdeprecations'):
# If the user does not want to get AiiDA deprecation warnings, we disable them - this can be achieved with::
# verdi config warnings.showdeprecations False
# Note that the AiidaDeprecationWarning does NOT inherit from DeprecationWarning
warnings.simplefilter('default', AiidaDeprecationWarning)
# This should default to 'once', i.e. once per different message
else:
warnings.simplefilter('ignore', AiidaDeprecationWarning)

return CONFIG
Loading

0 comments on commit d25c096

Please sign in to comment.