Skip to content

Commit

Permalink
Use apipkg to provide compatibility forwarders with v1 (#151)
Browse files Browse the repository at this point in the history
* Use apipkg to provide compatibility forwarders with v1

* add simple test

* add news

* patch conda via pip; add apipkg

* pre-commit

* try a different approach

* remove unused key

* yet one more

* quote < symbols

* quotess

* reorganize a bit to facilitate exports

* pre-commit

* move comment

* reorganize again

* move again to api

* add missing import

* vendor apipkg

* pre-commit

* Update apipkg to v3.0.1

* fix import

* vendor via vendoring pkg
  • Loading branch information
jaimergp authored Sep 3, 2023
1 parent 388e732 commit 78c4041
Show file tree
Hide file tree
Showing 18 changed files with 502 additions and 72 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ jobs:
--file tests/requirements.txt \
python=${{ matrix.python-version }}
- name: Patch conda with PR#11882 (TEMPORARY)
shell: bash -el {0}
run: |
pip install -U --no-deps https://github.com/conda/conda/archive/cep-menuinst.tar.gz
- shell: bash -el {0}
name: Conda info
run: |
Expand Down Expand Up @@ -119,7 +124,7 @@ jobs:
# Explicitly use Python 3.11 since each of the OSes has a different default Python
- uses: actions/setup-python@v4
with:
python-version: '3.11'
python-version: "3.11"

- name: Detect label
shell: python
Expand Down Expand Up @@ -153,4 +158,4 @@ jobs:
anaconda-org-channel: conda-canary
anaconda-org-label: ${{ env.ANACONDA_ORG_LABEL }}
anaconda-org-token: ${{ secrets.ANACONDA_ORG_CONDA_CANARY_TOKEN }}
conda-build-arguments: '--override-channels -c conda-forge -c defaults'
conda-build-arguments: "--override-channels -c conda-forge -c defaults"
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ repos:
rev: 6.1.0
hooks:
- id: flake8
exclude: ^menuinst/_vendor/.*$
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ include menuinst/_version.py
include menuinst/data/*.json
include menuinst/data/osx_launcher_*
include menuinst/data/appkit_launcher_*
include menuinst/_vendor/apipkg/LICENSE
exclude *.h *.cpp
1 change: 1 addition & 0 deletions dev/vendoring/vendor.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
apipkg==3.0.1
84 changes: 36 additions & 48 deletions menuinst/__init__.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,47 @@
"""
"""

import json
import os
import sys
from logging import basicConfig as _basicConfig
from logging import getLogger as _getLogger
from os import PathLike
from logging import basicConfig, getLogger

from .api import _install_adapter as install

try:
from ._version import __version__
except ImportError:
__version__ = "dev"


from ._legacy import install as _legacy_install
from .api import install as _api_install
from .api import remove as _api_remove
from .utils import DEFAULT_BASE_PREFIX, DEFAULT_PREFIX

_log = _getLogger(__name__)


log = getLogger(__name__)
if os.environ.get("MENUINST_DEBUG"):
_basicConfig(level="DEBUG")


def install(path: PathLike, remove: bool = False, prefix: PathLike = DEFAULT_PREFIX, **kwargs):
"""
This function is only here as a legacy adapter for menuinst v1.x.
Please use `menuinst.api` functions instead.
"""
if sys.platform == "win32":
path = path.replace("/", "\\")
json_path = os.path.join(prefix, path)
with open(json_path) as f:
metadata = json.load(f)
if "$id" not in metadata: # old style JSON
if sys.platform == "win32":
kwargs.setdefault("root_prefix", kwargs.pop("base_prefix", DEFAULT_BASE_PREFIX))
if kwargs["root_prefix"] is None:
kwargs["root_prefix"] = DEFAULT_BASE_PREFIX
_legacy_install(json_path, remove=remove, prefix=prefix, **kwargs)
else:
_log.warn(
"menuinst._legacy is only supported on Windows. "
"Switch to the new-style menu definitions "
"for cross-platform compatibility."
)
else:
# patch kwargs to reroute root_prefix to base_prefix
kwargs.setdefault("base_prefix", kwargs.pop("root_prefix", DEFAULT_BASE_PREFIX))
if kwargs["base_prefix"] is None:
kwargs["base_prefix"] = DEFAULT_BASE_PREFIX
if remove:
_api_remove(metadata, target_prefix=prefix, **kwargs)
else:
_api_install(metadata, target_prefix=prefix, **kwargs)
basicConfig(level="DEBUG")

__all__ = ["install", "__version__"]


# Compatibility forwarders for menuinst v1.x (Windows only)
if os.name == "nt":
from ._vendor.apipkg import initpkg

initpkg(
__name__,
exportdefs={
"win32": {
"dirs_src": "menuinst.platforms.win_utils.knownfolders:dirs_src",
},
"knownfolders": {
"get_folder_path": "menuinst.platforms.win_utils.knownfolders:get_folder_path",
"FOLDERID": "menuinst.platforms.win_utils.knownfolders:FOLDERID",
},
"winshortcut": {
"create_shortcut": "menuinst.platforms.win_utils.winshortcut:create_shortcut",
},
"win_elevate": {
"runAsAdmin": "menuinst.platforms.win_utils.win_elevate:runAsAdmin",
"isUserAdmin": "menuinst.platforms.win_utils.win_elevate:isUserAdmin",
},
},
# Calling initpkg WILL CLEAR the 'menuinst' top-level namespace, and only then will add
# the exportdefs contents! If we want to keep something defined in this module, we MUST
# make sure it's added in the 'attr' dictionary below.
attr={"__version__": __version__, "install": install},
)
18 changes: 18 additions & 0 deletions menuinst/_vendor/apipkg/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
39 changes: 39 additions & 0 deletions menuinst/_vendor/apipkg/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
apipkg: control the exported namespace of a Python package.
see https://pypi.python.org/pypi/apipkg
(c) holger krekel, 2009 - MIT license
"""
from __future__ import annotations

__all__ = ["initpkg", "ApiModule", "AliasModule", "__version__", "distribution_version"]
import sys
from typing import Any

from ._alias_module import AliasModule
from ._importing import distribution_version as distribution_version
from ._module import _initpkg
from ._module import ApiModule
from ._version import version as __version__


def initpkg(
pkgname: str,
exportdefs: dict[str, Any],
attr: dict[str, object] | None = None,
eager: bool = False,
) -> ApiModule:
"""initialize given package from the export definitions."""
attr = attr or {}
mod = sys.modules.get(pkgname)

mod = _initpkg(mod, pkgname, exportdefs, attr=attr)

# eagerload in bypthon to avoid their monkeypatching breaking packages
if "bpython" in sys.modules or eager:
for module in list(sys.modules.values()):
if isinstance(module, ApiModule):
getattr(module, "__dict__")

return mod
41 changes: 41 additions & 0 deletions menuinst/_vendor/apipkg/_alias_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

from types import ModuleType

from ._importing import importobj


def AliasModule(modname: str, modpath: str, attrname: str | None = None) -> ModuleType:

cached_obj: object | None = None

def getmod() -> object:
nonlocal cached_obj
if cached_obj is None:
cached_obj = importobj(modpath, attrname)
return cached_obj

x = modpath + ("." + attrname if attrname else "")
repr_result = f"<AliasModule {modname!r} for {x!r}>"

class AliasModule(ModuleType):
def __repr__(self) -> str:
return repr_result

def __getattribute__(self, name: str) -> object:
try:
return getattr(getmod(), name)
except ImportError:
if modpath == "pytest" and attrname is None:
# hack for pylibs py.test
return None
else:
raise

def __setattr__(self, name: str, value: object) -> None:
setattr(getmod(), name, value)

def __delattr__(self, name: str) -> None:
delattr(getmod(), name)

return AliasModule(str(modname))
41 changes: 41 additions & 0 deletions menuinst/_vendor/apipkg/_importing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

import os
import sys


def _py_abspath(path: str) -> str:
"""
special version of abspath
that will leave paths from jython jars alone
"""
if path.startswith("__pyclasspath__"):
return path
else:
return os.path.abspath(path)


def distribution_version(name: str) -> str | None:
"""try to get the version of the named distribution,
returns None on failure"""
if sys.version_info >= (3, 8):
from importlib.metadata import PackageNotFoundError, version
else:
from importlib_metadata import PackageNotFoundError, version
try:
return version(name)
except PackageNotFoundError:
return None


def importobj(modpath: str, attrname: str | None) -> object:
"""imports a module, then resolves the attrname on it"""
module = __import__(modpath, None, None, ["__doc__"])
if not attrname:
return module

retval = module
names = attrname.split(".")
for x in names:
retval = getattr(retval, x)
return retval
Loading

0 comments on commit 78c4041

Please sign in to comment.