|
| 1 | +--- |
| 2 | +myst: |
| 3 | + html_meta: |
| 4 | + "description": "How to convert a Python distribution from a pkg_resources namespace to a native namespace" |
| 5 | + "property=og:description": "How to convert a Python distribution from a pkg_resources namespace to a native namespace" |
| 6 | + "property=og:title": "Native namespace" |
| 7 | + "keywords": "Plone 6, developer guide, native namespaces, pkg_resources, Python" |
| 8 | +--- |
| 9 | + |
| 10 | +# Native namespace |
| 11 | + |
| 12 | +This chapter is guide for how to convert a Python distribution from a `pkg_resources` namespace to a native namespace. |
| 13 | + |
| 14 | +Python 3.3 added support for native namespaces. |
| 15 | +See [PEP 420](https://peps.python.org/pep-0420/) for more details. |
| 16 | + |
| 17 | +Plone has been using `pkg_resources`-style namespaces, but they are deprecated in `setuptools`. |
| 18 | +`setuptools` is planning to remove `pkg_resources`'s namespace support by the end of 2025. |
| 19 | +[PLIP 3928](https://github.com/plone/Products.CMFPlone/issues/3928) tracks the changes needed for Plone to adapt to the _new_ native namespaces. |
| 20 | + |
| 21 | +To convert a given Python distribution to use a native namespace, follow these steps. |
| 22 | + |
| 23 | + |
| 24 | +## Create maintenance branch |
| 25 | + |
| 26 | +```{note} |
| 27 | +This step is relevant only for Plone core packages. |
| 28 | +``` |
| 29 | + |
| 30 | +```{tip} |
| 31 | +This part is only needed when the `main` or `master` branch is used on multiple versions of the Plone core development buildout. |
| 32 | +``` |
| 33 | + |
| 34 | +If you haven't cloned the package's repository, then do so. |
| 35 | + |
| 36 | +```shell |
| 37 | +git clone git@github.com:plone/$package |
| 38 | +cd $package |
| 39 | +``` |
| 40 | + |
| 41 | +Otherwise, pull the latest changes into your local repository. |
| 42 | + |
| 43 | +```shell |
| 44 | +git fetch -p |
| 45 | +git checkout main # or master |
| 46 | +git rebase |
| 47 | +``` |
| 48 | + |
| 49 | +Find the last release tag, and create a branch for it. |
| 50 | + |
| 51 | +List all tags. |
| 52 | + |
| 53 | +```shell |
| 54 | +git for-each-ref --sort=taggerdate --format '%(tag)' refs/tags |
| 55 | +``` |
| 56 | + |
| 57 | +Get the last tag's major number. |
| 58 | + |
| 59 | +```shell |
| 60 | +MAJOR=`git for-each-ref --sort=taggerdate --format '%(tag)' refs/tags | tail -n1 | cut -d"." -f1` |
| 61 | +``` |
| 62 | + |
| 63 | +Create a branch for it. |
| 64 | + |
| 65 | +```shell |
| 66 | +git checkout -b $MAJOR.x |
| 67 | +``` |
| 68 | + |
| 69 | +Push the newly created branch. |
| 70 | + |
| 71 | +```shell |
| 72 | +git push |
| 73 | +``` |
| 74 | + |
| 75 | + |
| 76 | +## Update `buildout.coredev` |
| 77 | + |
| 78 | +```{note} |
| 79 | +This step is relevant only for Plone core packages. |
| 80 | +``` |
| 81 | + |
| 82 | +Update `buildout.coredev`'s branch `6.1` to use the newly created branch. |
| 83 | + |
| 84 | +```shell |
| 85 | +cd buildout.coredev |
| 86 | +``` |
| 87 | + |
| 88 | +Ensure you have the latest changes. |
| 89 | + |
| 90 | +```shell |
| 91 | +git fetch -p |
| 92 | +git checkout 6.1 |
| 93 | +git rebase |
| 94 | +``` |
| 95 | + |
| 96 | +Update the branch being used by the Python distribution. |
| 97 | + |
| 98 | +```shell |
| 99 | +sed -i "s/$package.git branch=master/$package.git branch=$MAJOR.x/" sources.cfg |
| 100 | +``` |
| 101 | + |
| 102 | +Add the changes, commit, and push. |
| 103 | + |
| 104 | +```shell |
| 105 | +git add sources.cfg |
| 106 | +git commit -m"chore: use branch $MAJOR.x for $package" |
| 107 | +git push |
| 108 | +``` |
| 109 | + |
| 110 | +Now check if you need to do the same on the `6.0` branch of `buildout.coredev`. |
| 111 | + |
| 112 | +```{tip} |
| 113 | +You can use this [handy table](https://jenkins.plone.org/roboto/branches) to know which branch is used by a given package for each Plone version. |
| 114 | +``` |
| 115 | + |
| 116 | +```{tip} |
| 117 | +To lower the amount of builds in Jenkins, either do a few at a time or add a `[ci-skip]` on the commit message. |
| 118 | +``` |
| 119 | + |
| 120 | +## Counts before change |
| 121 | + |
| 122 | +There is a risk when changing to the native namespaces that some files, or tests, might be left behind. |
| 123 | +To ensure all tests and files are kept after the switch, gather the counts before the change. |
| 124 | + |
| 125 | +```shell |
| 126 | +tox run -e test -- --list-tests | wc -l |
| 127 | +``` |
| 128 | + |
| 129 | +```{note} |
| 130 | +Adapt to whichever way you are using to run the tests. |
| 131 | +The above is meant for repositories that follow `plone.meta` conventions. |
| 132 | +The `--list-tests` comes from `zope.testrunner`, if you are using that, but not `plone.meta`, although you can use that as well. |
| 133 | +``` |
| 134 | + |
| 135 | +Create a distribution to get a listing of how many files are currently packaged. |
| 136 | + |
| 137 | +```shell |
| 138 | +rm -rf dist/ |
| 139 | +uvx --from build pyproject-build |
| 140 | +``` |
| 141 | + |
| 142 | +A `dist` folder is created with two archives in it, one each with the file suffix of `.tar.gz` and `.whl`. |
| 143 | +To get the count of files in them, run the following commands. |
| 144 | + |
| 145 | +```shell |
| 146 | +python -c "import glob; import tarfile; print(len(tarfile.open(glob.glob('dist/*.tar.gz')[0], 'r:gz').getnames()))" |
| 147 | +python -c "import glob; from zipfile import ZipFile; print(len(ZipFile(glob.glob('dist/*.whl')[0]).namelist()))" |
| 148 | +``` |
| 149 | + |
| 150 | +Note these counts for later in the step {ref}`compare-counts-after-change`. |
| 151 | + |
| 152 | + |
| 153 | +## Build backend |
| 154 | + |
| 155 | +To ensure the package continues to build, verify that `setuptools` is defined as its build backend. |
| 156 | +For that, inspect the {file}`pyproject.toml`. |
| 157 | +It should have the following lines. |
| 158 | + |
| 159 | +```toml |
| 160 | +[build-system] |
| 161 | +requires = ["setuptools>=68.2,<80", "wheel"] |
| 162 | +``` |
| 163 | + |
| 164 | +If they are not there, then add them, commit, and push the changes. |
| 165 | + |
| 166 | + |
| 167 | +## Convert to native namespace |
| 168 | + |
| 169 | +Use `plone.meta`'s `switch-to-pep420` script. |
| 170 | + |
| 171 | +```shell |
| 172 | +cd $package |
| 173 | +uvx --from plone.meta switch-to-pep420 --no-tests . |
| 174 | +``` |
| 175 | + |
| 176 | +```{tip} |
| 177 | +This will also bump the version to a new major release. |
| 178 | +
|
| 179 | +If the `main` or `master` branch is already an alpha version that is only used in Plone 6.2, you can specify that you don't want this version bump by adding the `--no-breaking` option. |
| 180 | +``` |
| 181 | + |
| 182 | + |
| 183 | +## Update the test matrix |
| 184 | + |
| 185 | +```{note} |
| 186 | +This step is relevant only for Plone core packages. |
| 187 | +``` |
| 188 | + |
| 189 | +Because the switch to the native namespace must be coordinated, all Python distributions need to be only for the same Plone version. |
| 190 | +In this case, it was decided to do it for the Plone 6.2 version. |
| 191 | +Thus, we need to ensure that the test matrix only tests against this Plone version. |
| 192 | +For that, update {file}`.meta.toml` with the following changes. |
| 193 | + |
| 194 | +```toml |
| 195 | +[tox] |
| 196 | +test_matrix = {"6.2" = ["*"]} |
| 197 | +``` |
| 198 | + |
| 199 | +Update the scaffolding files with `plone.meta`. |
| 200 | + |
| 201 | +```shell |
| 202 | +uvx --from plone.meta config-package --branch current . |
| 203 | +``` |
| 204 | + |
| 205 | +Review the changes and ensure all changes are sound. |
| 206 | + |
| 207 | +```{note} |
| 208 | +If the diff is quite big, then run `config-package` before all the changes. |
| 209 | +Get the result into a pull request, approved, and merged. |
| 210 | +Then do a follow up pull request to move it to a native namespace. |
| 211 | +``` |
| 212 | + |
| 213 | + |
| 214 | +(compare-counts-after-change)= |
| 215 | + |
| 216 | +## Compare counts after change |
| 217 | + |
| 218 | +Get the list of tests, and compare this result to the earlier one to ensure the same test counts. |
| 219 | + |
| 220 | +```shell |
| 221 | +tox run -e test -- --list-tests | wc -l |
| 222 | +``` |
| 223 | + |
| 224 | +Similarly, create the distribution files and compare the numbers with the previous run. |
| 225 | + |
| 226 | +```shell |
| 227 | +rm -rf dist/ |
| 228 | +uvx --from build pyproject-build |
| 229 | +python -c "import glob; import tarfile; print(len(tarfile.open(glob.glob('dist/*.tar.gz')[0], 'r:gz').getnames()))" |
| 230 | +python -c "import glob; from zipfile import ZipFile; print(len(ZipFile(glob.glob('dist/*.whl')[0]).namelist()))" |
| 231 | +``` |
| 232 | + |
| 233 | +It is okay if the numbers are slightly lower. |
| 234 | +For obvious reasons, the old source distribution will have one or more extra {file}`__init__.py` files. |
| 235 | +Both old distributions are expected to have an extra {file}`namespace_packages.txt` file and possibly an {file}`x-nspkg.pth` file. |
| 236 | +If you lose more than a few files though, something is wrong. |
| 237 | + |
| 238 | +If the numbers are close enough, review the changes once more, and push the branch with the changes for others to review it. |
0 commit comments