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

Move apply_patch() to reduce chance for circular imports #391

Merged
merged 1 commit into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ There is one special sub-package in `datalad-next`: `patches`. All runtime patch

### Runtime patches

The `patches` sub-package contains all runtime patches that are applied by `datalad-next`. Patches are applied on-import of `datalad-next`, and may modify arbitrary aspects of the runtime environment. A patch is enabled by adding a corresponding `import` statement to `datalad_next/patches/__init__.py`. The order of imports in this file is significant. New patches should consider behavior changes caused by other patches, and should be considerate of changes imposed on other patches.
The `patches` sub-package contains all runtime patches that are applied by `datalad-next`. Patches are applied on-import of `datalad-next`, and may modify arbitrary aspects of the runtime environment. A patch is enabled by adding a corresponding `import` statement to `datalad_next/patches/enabled.py`. The order of imports in this file is significant. New patches should consider behavior changes caused by other patches, and should be considerate of changes imposed on other patches.

`datalad-next` is imported (and thereby its patches applied) whenever used
directly (e.g., when running commands provided by `datalad-next`, or by an
Expand All @@ -73,7 +73,7 @@ DataLad core package itself when the configuration item

Patches modify an external implementation that is itself subject to change. To improve the validity and longevity of patches, it is helpful to consider a few guidelines:

- Patches should use `datalad_next.utils.apply_patch()` to perform the patching, in order to yield uniform (logging) behavior
- Patches should use `datalad_next.patches.apply_patch()` to perform the patching, in order to yield uniform (logging) behavior

- Patches should be as self-contained as possible. The aim is for patches to be merged upstream (at the patched entity) as quickly as possible. Self-contained patches facilitate this process.

Expand Down
2 changes: 1 addition & 1 deletion datalad_next/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@


# patch datalad-core
import datalad_next.patches
import datalad_next.patches.enabled

# register additional configuration items in datalad-core
from datalad.support.extensions import register_config
Expand Down
87 changes: 74 additions & 13 deletions datalad_next/patches/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,74 @@
from . import (
commanderror,
common_cfg,
annexrepo,
configuration,
create_sibling_ghlike,
interface_utils,
push_to_export_remote,
push_optimize,
siblings,
test_keyring,
customremotes_main,
)
from __future__ import annotations

from importlib import import_module
import logging
from typing import Any

lgr = logging.getLogger('datalad.ext.next.patches')


def apply_patch(
modname: str,
objname: str | None,
attrname: str,
patch: Any,
msg: str | None = None,
expect_attr_present=True,
):
"""
Monkey patch helper

Parameters
----------
modname: str
Importable name of the module with the patch target.
objname: str or None
If `None`, patch will target an attribute of the module,
otherwise an object name within the module can be specified.
attrname: str
Name of the attribute to replace with the patch, either
in the module or the given object (see ``objname``)
patch:
The identified attribute will be replaced with this object.
msg: str or None
If given, a debug-level log message with this text will be
emitted, otherwise a default message is generated.
expect_attr_present: bool
If True (default) an exception is raised when the target
attribute is not found.

Returns
-------
object
The original, unpatched attribute -- if ``expect_attr_present``
is enabled, or ``None`` otherwise.

Raises
------
ImportError
When the target module cannot be imported
AttributedError
When the target object is not found, or the target attribute is
not found.
"""
orig_attr = None

msg = msg or f'Apply patch to {modname}.{attrname}'
lgr.debug(msg)

# we want to fail on ImportError
mod = import_module(modname, package='datalad')
if objname:
# we want to fail on a missing object
obj = getattr(mod, objname)
else:
# the target is the module itself
obj = mod

if expect_attr_present:
# we want to fail on a missing attribute/object
orig_attr = getattr(obj, attrname)

setattr(obj, attrname, patch)

return orig_attr
2 changes: 1 addition & 1 deletion datalad_next/patches/annexrepo.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
get_specialremote_credential_properties,
needs_specialremote_credential_envpatch,
)
from datalad_next.utils.patch import apply_patch
from . import apply_patch


# reuse logger from -core, despite the unconventional name
Expand Down
2 changes: 1 addition & 1 deletion datalad_next/patches/customremotes_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Type,
)

from datalad_next.utils.patch import apply_patch
from . import apply_patch
from datalad_next.annexremotes import SpecialRemote


Expand Down
2 changes: 1 addition & 1 deletion datalad_next/patches/distribution_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import logging

from datalad_next.utils.patch import apply_patch
from . import apply_patch

# use same logger as -core, looks weird but is correct
lgr = logging.getLogger('datalad.dataset')
Expand Down
13 changes: 13 additions & 0 deletions datalad_next/patches/enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from . import (
commanderror,
common_cfg,
annexrepo,
configuration,
create_sibling_ghlike,
interface_utils,
push_to_export_remote,
push_optimize,
siblings,
test_keyring,
customremotes_main,
)
2 changes: 1 addition & 1 deletion datalad_next/patches/interface_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
_process_results,
)
from datalad_next.exceptions import IncompleteResultsError
from datalad_next.utils.patch import apply_patch
from . import apply_patch
from datalad_next.constraints.dataset import DatasetParameter

# use same logger as -core
Expand Down
2 changes: 1 addition & 1 deletion datalad_next/patches/push_optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
LegacyAnnexRepo as AnnexRepo,
Dataset,
)
from datalad_next.utils.patch import apply_patch
from . import apply_patch


lgr = logging.getLogger('datalad.core.distributed.push')
Expand Down
2 changes: 1 addition & 1 deletion datalad_next/patches/push_to_export_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
get_specialremote_credential_properties,
needs_specialremote_credential_envpatch,
)
from datalad_next.utils.patch import apply_patch
from . import apply_patch


lgr = logging.getLogger('datalad.core.distributed.push')
Expand Down
2 changes: 1 addition & 1 deletion datalad_next/patches/siblings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
AccessFailedError,
CapturedException,
)
from datalad_next.utils.patch import apply_patch
from . import apply_patch


# use same logger as -core
Expand Down
76 changes: 2 additions & 74 deletions datalad_next/utils/patch.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,2 @@
from __future__ import annotations

from importlib import import_module
import logging
from typing import Any

lgr = logging.getLogger('datalad.ext.next.utils.patch')


def apply_patch(
modname: str,
objname: str | None,
attrname: str,
patch: Any,
msg: str | None = None,
expect_attr_present=True,
):
"""
Monkey patch helper

Parameters
----------
modname: str
Importable name of the module with the patch target.
objname: str or None
If `None`, patch will target an attribute of the module,
otherwise an object name within the module can be specified.
attrname: str
Name of the attribute to replace with the patch, either
in the module or the given object (see ``objname``)
patch:
The identified attribute will be replaced with this object.
msg: str or None
If given, a debug-level log message with this text will be
emitted, otherwise a default message is generated.
expect_attr_present: bool
If True (default) an exception is raised when the target
attribute is not found.

Returns
-------
object
The original, unpatched attribute -- if ``expect_attr_present``
is enabled, or ``None`` otherwise.

Raises
------
ImportError
When the target module cannot be imported
AttributedError
When the target object is not found, or the target attribute is
not found.
"""
orig_attr = None

msg = msg or f'Apply patch to {modname}.{attrname}'
lgr.debug(msg)

# we want to fail on ImportError
mod = import_module(modname, package='datalad')
if objname:
# we want to fail on a missing object
obj = getattr(mod, objname)
else:
# the target is the module itself
obj = mod

if expect_attr_present:
# we want to fail on a missing attribute/object
orig_attr = getattr(obj, attrname)

setattr(obj, attrname, patch)

return orig_attr
# legacy import
from datalad_next.patches import apply_patch