diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e394c81 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,41 @@ +name: Run Tox Tests + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install depenencies (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y mercurial subversion + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + + - name: Test with tox + run: tox diff --git a/.gitignore b/.gitignore index 2e2b2e4..d2b69bc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ /include/ /.installed.cfg /lib/ -/parts/ \ No newline at end of file +/parts/ +/.python-version diff --git a/CHANGES.rst b/CHANGES.rst index 3e416b5..559dfbd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changelog 2.0.3 (unreleased) ------------------ -- Nothing changed yet. +* Add ``subpath`` option to specify a subdirectory of a repository. [mamico] 2.0.2 (2024-04-24) diff --git a/README.rst b/README.rst index 1a366bd..bae4ad7 100644 --- a/README.rst +++ b/README.rst @@ -127,6 +127,9 @@ Common options those which start with the prefix. These two options currently only work for ``cvs`` and ``hg``. + The ``subpath`` option allows you to specify a subdirectory of a repository + as the source for the Python package, ideal for monorepos. + ``svn`` The ``url`` is one of the urls supported by subversion. diff --git a/setup.cfg b/setup.cfg index f072136..1b713b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,9 @@ version-levels = 2 [devpi:upload] formats = sdist.tgz,bdist_wheel +[flake8] +extend-ignore = E501 + [check-manifest] ignore = build_git.sh diff --git a/src/mr/developer/common.py b/src/mr/developer/common.py index 6bb548e..08d7d49 100644 --- a/src/mr/developer/common.py +++ b/src/mr/developer/common.py @@ -182,16 +182,16 @@ def worker(working_copies, the_queue): # See GitHub issue # 210 # wc._output is a list containing n-length tuples which are messages from the thread. # each tuple (item) first position is a logger function - # the rest of the tuple is the message. - - # In cases where the message tuple has more than 2 elements in it - # (logger, message, message, ... ) + # the rest of the tuple is the message. + + # In cases where the message tuple has more than 2 elements in it + # (logger, message, message, ... ) # then all messages are joined. for item in wc._output: lvl = item[0] msg = ','.join(item[1:]) lvl(msg) - + if kwargs.get('verbose', False) and output is not None and output.strip(): if six.PY3 and isinstance(output, six.binary_type): output = output.decode('utf8') diff --git a/src/mr/developer/extension.py b/src/mr/developer/extension.py index d37594a..26c2c60 100644 --- a/src/mr/developer/extension.py +++ b/src/mr/developer/extension.py @@ -195,6 +195,8 @@ def get_develop_info(self): source = sources[name] if source.get('egg', True) and name not in develeggs: path = sources[name]['path'] + if sources[name].get("subpath"): + path = os.path.join(path, sources[name]["subpath"]) status = config_develop.get(name, name in auto_checkout) if os.path.exists(path) and status: if name in auto_checkout: diff --git a/src/mr/developer/tests/conftest.py b/src/mr/developer/tests/conftest.py index a36306a..2ee19c6 100644 --- a/src/mr/developer/tests/conftest.py +++ b/src/mr/developer/tests/conftest.py @@ -8,6 +8,9 @@ class Path(str): def __getitem__(self, name): return Path(os.path.join(self, name)) + def create_dir(self): + os.makedirs(str(self)) + def create_file(self, *content): f = open(self, 'w') f.write('\n'.join(content)) diff --git a/src/mr/developer/tests/test_extension.py b/src/mr/developer/tests/test_extension.py index 0ebdb80..0ffed5b 100644 --- a/src/mr/developer/tests/test_extension.py +++ b/src/mr/developer/tests/test_extension.py @@ -299,6 +299,38 @@ def testDevelopSourcesMix(self, buildout, extension): _exists.__exit__(None, None, None) assert develop == ['/normal/develop', '/develop/with/slash/', 'src/pkg.bar'] + def testDevelopMonorepo(self, buildout, extension): + buildout['sources'].update({ + 'pkg.bar': 'git dummy://foo subpath=bar'}) + buildout['buildout']['auto-checkout'] = 'pkg.bar' + _exists = patch('os.path.exists') + exists = _exists.__enter__() + try: + exists().return_value = True + (develop, develeggs, versions) = extension.get_develop_info() + finally: + _exists.__exit__(None, None, None) + assert develop == ['src/pkg.bar/bar'] + assert develeggs == {'pkg.bar': '/buildout/src/pkg.bar/bar'} + assert versions == {'pkg.bar': ''} + + def testDevelopMonorepoTwoRepos(self, buildout, extension): + buildout['sources'].update({ + 'pkg.bar': 'git dummy://monorepo subpath=bar', + 'pkg.foo': 'git dummy://monorepo subpath=foo', + }) + buildout['buildout']['auto-checkout'] = 'pkg.bar\npkg.foo' + _exists = patch('os.path.exists') + exists = _exists.__enter__() + try: + exists().return_value = True + (develop, develeggs, versions) = extension.get_develop_info() + finally: + _exists.__exit__(None, None, None) + assert develop == ['src/pkg.bar/bar', 'src/pkg.foo/foo'] + assert develeggs == {'pkg.bar': '/buildout/src/pkg.bar/bar', 'pkg.foo': '/buildout/src/pkg.foo/foo'} + assert versions == {'pkg.bar': '', 'pkg.foo': ''} + def testMissingSourceSection(self, buildout, extension): del buildout['sources'] assert extension.get_sources() == {} diff --git a/src/mr/developer/tests/test_git.py b/src/mr/developer/tests/test_git.py index 8fb77ca..77ac260 100644 --- a/src/mr/developer/tests/test_git.py +++ b/src/mr/developer/tests/test_git.py @@ -29,7 +29,6 @@ def createDefaultContent(self, repository): def testUpdateWithRevisionPin(self, develop, mkgitrepo, src): from mr.developer.commands import CmdCheckout from mr.developer.commands import CmdUpdate - from mr.developer.commands import CmdStatus repository = mkgitrepo('repository') rev = self.createDefaultContent(repository) @@ -48,6 +47,13 @@ def testUpdateWithRevisionPin(self, develop, mkgitrepo, src): shutil.rmtree(src['egg']) + def testUpdateWithBranch(self, develop, mkgitrepo, src): + from mr.developer.commands import CmdCheckout + from mr.developer.commands import CmdUpdate + from mr.developer.commands import CmdStatus + repository = mkgitrepo('repository') + self.createDefaultContent(repository) + # check branch develop.sources = { 'egg': Source( @@ -62,37 +68,26 @@ def testUpdateWithRevisionPin(self, develop, mkgitrepo, src): assert set(os.listdir(src['egg'])) == set(('.git', 'foo', 'foo2')) CmdStatus(develop)(develop.parser.parse_args(['status'])) - # switch implicitly to master branch + def testUpdateWithMain(self, develop, mkgitrepo, src): + from mr.developer.commands import CmdCheckout + from mr.developer.commands import CmdUpdate + repository = mkgitrepo('repository') + self.createDefaultContent(repository) develop.sources = { 'egg': Source( kind='git', name='egg', url='%s' % repository.base, path=src['egg'])} - CmdUpdate(develop)(develop.parser.parse_args(['up', 'egg'])) + CmdCheckout(develop)(develop.parser.parse_args(['co', 'egg'])) assert set(os.listdir(src['egg'])) == set(('.git', 'bar', 'foo')) - - # Switch to specific revision, then switch back to master branch. - develop.sources = { - 'egg': Source( - kind='git', - name='egg', - rev=rev, - url='%s' % repository.base, - path=src['egg'])} - CmdUpdate(develop)(develop.parser.parse_args(['up', 'egg'])) - assert set(os.listdir(src['egg'])) == set(('.git', 'foo', 'foo2')) - develop.sources = { - 'egg': Source( - kind='git', - name='egg', - url='%s' % repository.base, - path=src['egg'])} CmdUpdate(develop)(develop.parser.parse_args(['up', 'egg'])) assert set(os.listdir(src['egg'])) == set(('.git', 'bar', 'foo')) - CmdStatus(develop)(develop.parser.parse_args(['status'])) - + def testRaiseExceptionUpdateWithRevisionAndBranch(self, develop, mkgitrepo, src): + from mr.developer.commands import CmdCheckout + repository = mkgitrepo('repository') + rev = self.createDefaultContent(repository) # we can't use both rev and branch with pytest.raises(SystemExit): develop.sources = { @@ -169,10 +164,13 @@ def testUpdateVerbose(self, develop, mkgitrepo, src, capsys): ('info', ("Updated 'egg' with git.",), {}), ('info', ("Switching to remote branch 'remotes/origin/master'.",), {})] captured = capsys.readouterr() - older = "* develop\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nBranch master set up to track remote branch master from origin.\n develop\n* master\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nAlready up-to-date.\n\n" - newer = "* develop\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nBranch 'master' set up to track remote branch 'master' from 'origin'.\n develop\n* master\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nAlready up to date.\n\n" # git output varies between versions... - assert captured.out in [older, newer] + git_outputs = [ + "* develop\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nBranch master set up to track remote branch master from origin.\n develop\n* master\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nAlready up-to-date.\n\n", + "* develop\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nBranch 'master' set up to track remote branch 'master' from 'origin'.\n develop\n* master\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nAlready up to date.\n\n", + "* develop\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nbranch 'master' set up to track 'origin/master'.\n develop\n* master\n remotes/origin/HEAD -> origin/develop\n remotes/origin/develop\n remotes/origin/master\nAlready up to date.\n\n", + ] + assert captured.out in git_outputs CmdStatus(develop)(develop.parser.parse_args(['status', '-v'])) captured = capsys.readouterr() assert captured.out == "~ A egg\n ## master...origin/master\n\n" diff --git a/src/mr/developer/tests/test_mercurial.py b/src/mr/developer/tests/test_mercurial.py index 9125a43..b6ba6a5 100644 --- a/src/mr/developer/tests/test_mercurial.py +++ b/src/mr/developer/tests/test_mercurial.py @@ -91,6 +91,19 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): CmdUpdate(develop)(develop.parser.parse_args(['up', 'egg'])) assert set(os.listdir(os.path.join(src, 'egg'))) == set(('.hg', 'foo')) + def testUpdateWithBranch(self, develop, src, tempdir): + from mr.developer.commands import CmdCheckout + from mr.developer.commands import CmdUpdate + repository = tempdir['repository'] + os.mkdir(repository) + process = Process(cwd=repository) + process.check_call("hg init %s" % repository) + foo = repository['foo'] + foo.create_file('foo') + process.check_call("hg add %s" % foo, echo=False) + process.check_call("hg branch test", echo=False) + process.check_call("hg commit %s -m foo -u test" % foo, echo=False) + # check branch develop.sources = { 'egg': Source( @@ -104,6 +117,26 @@ def testUpdateWithRevisionPin(self, develop, src, tempdir): CmdUpdate(develop)(develop.parser.parse_args(['up', 'egg'])) assert set(os.listdir(os.path.join(src, 'egg'))) == set(('.hg', 'foo')) + def testUpdateRaiseWithRevAndBranch(self, develop, src, tempdir): + from mr.developer.commands import CmdCheckout + repository = tempdir['repository'] + os.mkdir(repository) + process = Process(cwd=repository) + process.check_call("hg init %s" % repository) + foo = repository['foo'] + foo.create_file('foo') + process.check_call("hg add %s" % foo, echo=False) + process.check_call("hg branch test", echo=False) + process.check_call("hg commit %s -m foo -u test" % foo, echo=False) + + # get comitted rev + lines = process.check_call("hg log %s" % foo, echo=False) + try: + # XXX older version + rev = lines[0].split()[1].split(b(':'))[1] + except Exception: + rev = lines[0].split()[1] + # we can't use both rev and branch with pytest.raises(SystemExit): develop.sources = { diff --git a/src/mr/developer/tests/utils.py b/src/mr/developer/tests/utils.py index 80586d3..a8bfb83 100644 --- a/src/mr/developer/tests/utils.py +++ b/src/mr/developer/tests/utils.py @@ -175,10 +175,13 @@ def __call__(self, cmd, **kw): def init(self): os.mkdir(self.base) self("git init") + self('git config --global init.defaultBranch master') + self('git config --global protocol.file.allow always') def setup_user(self): self('git config user.email "florian.schulze@gmx.net"') self('git config user.name "Florian Schulze"') + self('git config commit.gpgsign false') def add_file(self, fname, msg=None): repo_file = self.base[fname] @@ -188,6 +191,10 @@ def add_file(self, fname, msg=None): msg = fname self("git commit %s -m %s" % (repo_file, msg), echo=False) + def add_dir(self, dirname): + repo_dir = self.base[dirname] + repo_dir.create_dir() + def add_submodule(self, submodule, submodule_name): assert isinstance(submodule, GitRepo) self("git submodule add %s %s" % (submodule.url, submodule_name)) diff --git a/tox.ini b/tox.ini index 89c759f..3d27478 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,13 @@ [tox] -envlist = py27,py27-configparser,py34,py35,py36,py37 +envlist = py39,py310,py311,py312,py313 + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 [base] deps =