用不同的根名称为Python软件包设置别名,当旧的软件包使用延迟导入时。

huangapple go评论104阅读模式
英文:

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:

  1. ├── setup.py
  2. ├── pyproject.toml
  3. └── oldpkg
  4. ├── __init__.py
  5. ├── __main__.py
  6. ├── submod.py
  7. └── subpkg
  8. ├── __init__.py
  9. └── nested.py

My first thought was to introduce:

  1. ├── newpkg.py

and fill its contents with from oldpkg import *

Then in setup.py add:

  1. packages=find_packages(include=[
  2. 'oldpkg', 'oldpkg.*',
  3. # Alias the module while we transition to a new name.
  4. 'newpkg', 'newpkg.*',
  5. ]),

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:

  1. # In bash I'd like the CLI to be the same. The __main__.py should be mirrored
  2. python -m oldpkg
  3. python -m newpkg
  1. # In Python
  2. import oldpkg
  3. import newpkg
  4. assert oldpkg is newpkg
  5. from oldpkg.subpkg import nested as n1
  6. from newpkg.subpkg import nested as n2
  7. 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:

  1. newpkg is just a pointer to oldpkg.
  2. contents move from oldpkg to newpkg and now oldpkg is a pointer to newpkg.
  3. oldpkg now includes a deprecation warning.
  4. oldpkg now errors when it is imported and tells the user to use newpkg.
  5. 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:

  1. ├── setup.py
  2. ├── pyproject.toml
  3. └── oldpkg
  4. ├── __init__.py
  5. ├── __main__.py
  6. ├── submod.py
  7. └── subpkg
  8. ├── __init__.py
  9. └── nested.py

My first thought was to introduce:

  1. ├── newpkg.py

and fill its contents with from oldpkg import *

Then in setup.py add:

  1. packages=find_packages(include=[
  2. &#39;oldpkg&#39;, &#39;oldpkg.*&#39;,
  3. # Alias the module while we transition to a new name.
  4. &#39;newpkg&#39;, &#39;newpkg.*&#39;,
  5. ]),

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 &lt;oldpkg&gt;.&lt;subname&gt; 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:

  1. # In bash I&#39;d like the CLI to be the same. The `__main__.py` should be mirrored
  2. python -m oldpkg
  3. python -m newpkg
  1. # In Python
  2. import oldpkg
  3. import newpkg
  4. assert oldpkg is newpkg
  5. from oldpkg.subpkg import nested as n1
  6. from newpkg.subpkg import nested as n2
  7. 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:

  1. newpkg is just a pointer to oldpkg.
  2. contents move from oldpkg to newpkg and now oldpkg is a pointer to newpkg.
  3. oldpkg now includes a deprecation warning.
  4. oldpkg now errors when it is imported and tells the user to use newpkg.
  5. oldpkg is removed.

Is there any prior art on acomplishing this transition plan?

答案1

得分: 0

以下是翻译好的内容:

我已经创建了一个自动生成处理大多数情况所需的文件的脚本。它在python -m方面仍然存在问题,但我认为我可以找到一种方法使其工作。当我完成后,我会更新答案。

以下脚本创建了另一个模块,其中包含与您的模块完全相同的文件,并添加了一个__getattr__,以便所有这些模块属性都引用回原始模块。

  1. import networkx as nx
  2. import ubelt as ub
  3. new_name = 'NEW_MODULE'
  4. module = ub.import_module_from_name('OLD_MODULE')
  5. module_dpath = ub.Path(module.__file__).parent
  6. repo_dpath = module_dpath.parent
  7. g = nx.DiGraph()
  8. g.add_node(module_dpath, label=module_dpath.name, type='dir')
  9. for root, dnames, fnames in module_dpath.walk():
  10. if '__init__.py' not in fnames:
  11. dnames.clear()
  12. continue
  13. g.add_node(root, label=root.name, type='dir')
  14. if root != module_dpath:
  15. g.add_edge(root.parent, root)
  16. for f in fnames:
  17. if f.endswith('.py'):
  18. fpath = root / f
  19. g.add_node(fpath, label=fpath.name, type='file')
  20. g.add_edge(root, fpath)
  21. for p in list(g.nodes):
  22. node_data = g.nodes

  23. ntype = node_data.get('type', None)

  24. if ntype == 'dir':

  25. node_data['label'] = ub.color_text(node_data['label'], 'blue')

  26. elif ntype == 'file':

  27. node_data['label'] = ub.color_text(node_data['label'], 'green')

  28. nx.write_network_text(g)

  29. for node, node_data in g.nodes(data=True):

  30. if node_data['type'] == 'file':

  31. relpath = node.relative_to(module_dpath)

  32. new_fpath = repo_dpath / new_name / relpath

  33. new_fpath.parent.ensuredir()

  34. modname = ub.modpath_to_modname(node)

  35. print(f'new_fpath={new_fpath}')

  36. if new_fpath.name == '__main__.py':

  37. new_fpath.write_text(ub.codeblock(

  38. f'''

  39. from {modname} import * # NOQA

  40. '''))

  41. else:

  42. new_fpath.write_text(ub.codeblock(

  43. f'''

  44. # Autogenerated via:

  45. # python dev/maintain/mirror_package.py

  46. def __getattr__(key):

  47. import {modname} as mirror

  48. return getattr(mirror, key)

  49. '''))

这种方式虽然需要很多样板代码,但它是可行的,并且可以通过脚本进行维护。

英文:

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.

  1. import networkx as nx
  2. import ubelt as ub
  3. new_name = &#39;NEW_MODULE&#39;
  4. module = ub.import_module_from_name(&#39;OLD_MODULE&#39;)
  5. module_dpath = ub.Path(module.__file__).parent
  6. repo_dpath = module_dpath.parent
  7. g = nx.DiGraph()
  8. g.add_node(module_dpath, label=module_dpath.name, type=&#39;dir&#39;)
  9. for root, dnames, fnames in module_dpath.walk():
  10. # dnames[:] = [d for d in dnames if not dname_block_pattern.match(d)]
  11. if &#39;__init__.py&#39; not in fnames:
  12. dnames.clear()
  13. continue
  14. g.add_node(root, label=root.name, type=&#39;dir&#39;)
  15. if root != module_dpath:
  16. g.add_edge(root.parent, root)
  17. # for d in dnames:
  18. # dpath = root / d
  19. # g.add_node(dpath, label=dpath.name)
  20. # g.add_edge(root, dpath)
  21. for f in fnames:
  22. if f.endswith(&#39;.py&#39;):
  23. fpath = root / f
  24. g.add_node(fpath, label=fpath.name, type=&#39;file&#39;)
  25. g.add_edge(root, fpath)
  26. for p in list(g.nodes):
  27. node_data = g.nodes

  28. ntype = node_data.get(&#39;type&#39;, None)

  29. if ntype == &#39;dir&#39;:

  30. node_data[&#39;label&#39;] = ub.color_text(node_data[&#39;label&#39;], &#39;blue&#39;)

  31. elif ntype == &#39;file&#39;:

  32. node_data[&#39;label&#39;] = ub.color_text(node_data[&#39;label&#39;], &#39;green&#39;)

  33. nx.write_network_text(g)

  34. for node, node_data in g.nodes(data=True):

  35. if node_data[&#39;type&#39;] == &#39;file&#39;:

  36. relpath = node.relative_to(module_dpath)

  37. new_fpath = repo_dpath / new_name / relpath

  38. new_fpath.parent.ensuredir()

  39. modname = ub.modpath_to_modname(node)

  40. print(f&#39;new_fpath={new_fpath}&#39;)

  41. if new_fpath.name == &#39;__main__.py&#39;:

  42. new_fpath.write_text(ub.codeblock(

  43. f&#39;&#39;&#39;

  44. from {modname} import * # NOQA

  45. &#39;&#39;&#39;))

  46. else:

  47. new_fpath.write_text(ub.codeblock(

  48. f&#39;&#39;&#39;

  49. # Autogenerated via:

  50. # python dev/maintain/mirror_package.py

  51. def __getattr__(key):

  52. import {modname} as mirror

  53. return getattr(mirror, key)

  54. &#39;&#39;&#39;))

It's annoying that this takes so much boilerplate, but it works, and it is maintainable via a script.

huangapple
  • 本文由 发表于 2023年4月6日 22:26:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/75950681.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定