英文:
Alias Python package with a different root name when the old one uses lazy imports
问题
以下是您要翻译的内容:
"I'm currently in the middle of renaming a project. In the meantime I need to not break existing users, so I'd like to provide an alias that lets users use it exactly the same as they previously would. This is turning out to be tricker than I initially thought.
Say I have a repo with a package:
├── setup.py
├── pyproject.toml
└── oldpkg
├── __init__.py
├── __main__.py
├── submod.py
└── subpkg
├── __init__.py
└── nested.py
My first thought was to introduce:
├── newpkg.py
and fill its contents with from oldpkg import *
Then in setup.py add:
packages=find_packages(include=[
'oldpkg', 'oldpkg.*',
# Alias the module while we transition to a new name.
'newpkg', 'newpkg.*',
]),
so both packages are installed.
Works at a the top level for import newpkg
but fails if you from newpkg import subpkg
.
My next thought was that I could write a script that for every python file in the old package I autogenerate a dumy python file in the new package with from <oldpkg>.<subname> import *
.
Now this functionally works, but it introduces two namespaces and it has the problem that it actually accesses the attributes in the old package. The old package uses lazy imports, which means it uses the module level __getattr__
to expose submodules dynamically without doing the time consuming work of importing them all at startup.
I'm not sure if there is any way around this problem. Really all I want is the user to be able to use oldpkg
and newpkg
interchangably. In fact it would be really great if import oldpkg, newpkg; oldpkg is newpkg
was True.
Is there any way that I can write a newpkg
that is a strict alias of oldpkg
? If possible I'd like the following statements to be functionally equivalent:
# In bash I'd like the CLI to be the same. The __main__.py should be mirrored
python -m oldpkg
python -m newpkg
# In Python
import oldpkg
import newpkg
assert oldpkg is newpkg
from oldpkg.subpkg import nested as n1
from newpkg.subpkg import nested as n2
assert n1 is n2
Perhaps the above is not possible, but I'm wondering what the best way to go about this would be. I want to go through the following transition phases:
- newpkg is just a pointer to oldpkg.
- contents move from oldpkg to newpkg and now oldpkg is a pointer to newpkg.
- oldpkg now includes a deprecation warning.
- oldpkg now errors when it is imported and tells the user to use newpkg.
- oldpkg is removed.
Is there any prior art on acomplishing this transition plan?
请注意,我只提供翻译,不包括代码部分。如果需要任何进一步的帮助,请随时提问。
英文:
I'm currently in the middle of renaming a project. In the meantime I need to not break existing users, so I'd like to provide an alias that lets users use it exactly the same as they previously would. This is turning out to be tricker than I initially thought.
Say I have a repo with a package:
├── setup.py
├── pyproject.toml
└── oldpkg
├── __init__.py
├── __main__.py
├── submod.py
└── subpkg
├── __init__.py
└── nested.py
My first thought was to introduce:
├── newpkg.py
and fill its contents with from oldpkg import *
Then in setup.py add:
packages=find_packages(include=[
'oldpkg', 'oldpkg.*',
# Alias the module while we transition to a new name.
'newpkg', 'newpkg.*',
]),
so both packages are installed.
Works at a the top level for import newpkg
but fails if you from newpkg import subpkg
.
My next thought was that I could write a script that for every python file in the old package I autogenerate a dumy python file in the new package with from <oldpkg>.<subname> import *
.
Now this functionally works, but it introduces two namespaces and it has the problem that it actually accesses the attributes in the old package. The old package uses lazy imports, which means it uses the module level __getattr__
to expose submodules dynamically without doing the time consuming work of importing them all at startup.
I'm not sure if there is any way around this problem. Really all I want is the user to be able to use oldpkg
and newpkg
interchangably. In fact it would be really great if import oldpkg, newpkg; oldpkg is newpkg
was True.
Is there any way that I can write a newpkg
that is a strict alias of oldpkg
? If possible I'd like the following statements to be functionally equivalent:
# In bash I'd like the CLI to be the same. The `__main__.py` should be mirrored
python -m oldpkg
python -m newpkg
# In Python
import oldpkg
import newpkg
assert oldpkg is newpkg
from oldpkg.subpkg import nested as n1
from newpkg.subpkg import nested as n2
assert n1 is n2
Perhaps the above is not possible, but I'm wondering what the best way to go about this would be. I want to go through the following transition phases:
- newpkg is just a pointer to oldpkg.
- contents move from oldpkg to newpkg and now oldpkg is a pointer to newpkg.
- oldpkg now includes a deprecation warning.
- oldpkg now errors when it is imported and tells the user to use newpkg.
- oldpkg is removed.
Is there any prior art on acomplishing this transition plan?
答案1
得分: 0
以下是翻译好的内容:
我已经创建了一个自动生成处理大多数情况所需的文件的脚本。它在python -m
方面仍然存在问题,但我认为我可以找到一种方法使其工作。当我完成后,我会更新答案。
以下脚本创建了另一个模块,其中包含与您的模块完全相同的文件,并添加了一个__getattr__
,以便所有这些模块属性都引用回原始模块。
import networkx as nx
import ubelt as ub
new_name = 'NEW_MODULE'
module = ub.import_module_from_name('OLD_MODULE')
module_dpath = ub.Path(module.__file__).parent
repo_dpath = module_dpath.parent
g = nx.DiGraph()
g.add_node(module_dpath, label=module_dpath.name, type='dir')
for root, dnames, fnames in module_dpath.walk():
if '__init__.py' not in fnames:
dnames.clear()
continue
g.add_node(root, label=root.name, type='dir')
if root != module_dpath:
g.add_edge(root.parent, root)
for f in fnames:
if f.endswith('.py'):
fpath = root / f
g.add_node(fpath, label=fpath.name, type='file')
g.add_edge(root, fpath)
for p in list(g.nodes):
node_data = g.nodes
ntype = node_data.get('type', None)
if ntype == 'dir':
node_data['label'] = ub.color_text(node_data['label'], 'blue')
elif ntype == 'file':
node_data['label'] = ub.color_text(node_data['label'], 'green')
nx.write_network_text(g)
for node, node_data in g.nodes(data=True):
if node_data['type'] == 'file':
relpath = node.relative_to(module_dpath)
new_fpath = repo_dpath / new_name / relpath
new_fpath.parent.ensuredir()
modname = ub.modpath_to_modname(node)
print(f'new_fpath={new_fpath}')
if new_fpath.name == '__main__.py':
new_fpath.write_text(ub.codeblock(
f'''
from {modname} import * # NOQA
'''))
else:
new_fpath.write_text(ub.codeblock(
f'''
# Autogenerated via:
# python dev/maintain/mirror_package.py
def __getattr__(key):
import {modname} as mirror
return getattr(mirror, key)
'''))
这种方式虽然需要很多样板代码,但它是可行的,并且可以通过脚本进行维护。
英文:
I've come up with a script that autogenerates the files necessary to handle most cases. It still has issues with python -m
, but I think I can figure out a way to make it work. I will update the answer when I finish that.
The following script makes a another module with the same exact files as yours and adds a __getattr__
so all of those module attributes refer back ot the original module.
import networkx as nx
import ubelt as ub
new_name = 'NEW_MODULE'
module = ub.import_module_from_name('OLD_MODULE')
module_dpath = ub.Path(module.__file__).parent
repo_dpath = module_dpath.parent
g = nx.DiGraph()
g.add_node(module_dpath, label=module_dpath.name, type='dir')
for root, dnames, fnames in module_dpath.walk():
# dnames[:] = [d for d in dnames if not dname_block_pattern.match(d)]
if '__init__.py' not in fnames:
dnames.clear()
continue
g.add_node(root, label=root.name, type='dir')
if root != module_dpath:
g.add_edge(root.parent, root)
# for d in dnames:
# dpath = root / d
# g.add_node(dpath, label=dpath.name)
# g.add_edge(root, dpath)
for f in fnames:
if f.endswith('.py'):
fpath = root / f
g.add_node(fpath, label=fpath.name, type='file')
g.add_edge(root, fpath)
for p in list(g.nodes):
node_data = g.nodes
ntype = node_data.get('type', None)
if ntype == 'dir':
node_data['label'] = ub.color_text(node_data['label'], 'blue')
elif ntype == 'file':
node_data['label'] = ub.color_text(node_data['label'], 'green')
nx.write_network_text(g)
for node, node_data in g.nodes(data=True):
if node_data['type'] == 'file':
relpath = node.relative_to(module_dpath)
new_fpath = repo_dpath / new_name / relpath
new_fpath.parent.ensuredir()
modname = ub.modpath_to_modname(node)
print(f'new_fpath={new_fpath}')
if new_fpath.name == '__main__.py':
new_fpath.write_text(ub.codeblock(
f'''
from {modname} import * # NOQA
'''))
else:
new_fpath.write_text(ub.codeblock(
f'''
# Autogenerated via:
# python dev/maintain/mirror_package.py
def __getattr__(key):
import {modname} as mirror
return getattr(mirror, key)
'''))
It's annoying that this takes so much boilerplate, but it works, and it is maintainable via a script.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论