Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Unreleased

***Added:***

- The `index` publisher now recognizes repository-specific options

***Fixed:***

- Handle more edge cases in the `setuptools` migration script
Expand Down
18 changes: 17 additions & 1 deletion docs/plugins/publisher/package-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,20 @@ The publisher plugin name is `index`.
| `--ca-cert` | `ca-cert` | The path to a CA bundle |
| `--client-cert` | `client-cert` | The path to a client certificate, optionally containing the private key |
| `--client-key` | `client-key` | The path to the client certificate's private key |
| | `repos` | A table of named repositories to their respective URLs |
| | `repos` | A table of named [repositories](#repositories) to their respective options |

## Repositories

All top-level options can be overridden per repository using the `repos` table with a required `url` attribute for each repository. The following shows the default configuration:

=== ":octicons-file-code-16: config.toml"

```toml
[publish.index.repos.main]
url = "https://upload.pypi.org/legacy/"

[publish.index.repos.test]
url = "https://test.pypi.org/legacy/"
```

The `repo` and `repos` options have no effect.
4 changes: 2 additions & 2 deletions docs/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Rather than specifying the full URL of a repository, you can use a named reposit
=== ":octicons-file-code-16: config.toml"

```toml
[publish.index.repos]
repo1 = "url1"
[publish.index.repos.private]
url = "..."
...
```

Expand Down
2 changes: 1 addition & 1 deletion src/hatch/config/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def publish(self):

self._field_publish = publish
else:
self._field_publish = self.raw_data['publish'] = {'index': {'user': '', 'auth': ''}}
self._field_publish = self.raw_data['publish'] = {'index': {'repo': 'main'}}

return self._field_publish

Expand Down
53 changes: 39 additions & 14 deletions src/hatch/publish/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,33 @@
class IndexPublisher(PublisherInterface):
PLUGIN_NAME = 'index'

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.repos = self.plugin_config.get('repos', {}).copy()
self.repos['main'] = 'https://upload.pypi.org/legacy/'
self.repos['test'] = 'https://test.pypi.org/legacy/'
def get_repos(self):
global_plugin_config = self.plugin_config.copy()
defined_repos = self.plugin_config.pop('repos', {})
self.plugin_config.pop('repo', None)

# Normalize type
repos = {}
for repo, data in defined_repos.items():
if isinstance(data, str):
data = {'url': data}
elif not isinstance(data, dict):
self.app.abort(f'Hatch config field `publish.index.repos.{repo}` must be a string or a mapping')
elif 'url' not in data:
self.app.abort(f'Hatch config field `publish.index.repos.{repo}` must define a `url` key')

repos[repo] = data

# Ensure PyPI correct
for repo, url in (('main', 'https://upload.pypi.org/legacy/'), ('test', 'https://test.pypi.org/legacy/')):
repos.setdefault(repo, {})['url'] = url

# Populate defaults
for config in repos.values():
for key, value in global_plugin_config.items():
config.setdefault(key, value)

return repos

def publish(self, artifacts: list, options: dict):
"""
Expand All @@ -37,22 +58,26 @@ def publish(self, artifacts: list, options: dict):
else:
repo = self.plugin_config.get('repo', 'main')

if repo in self.repos:
repo = self.repos[repo]
repos = self.get_repos()

if repo in repos:
repo_config = repos[repo]
else:
repo_config = {'url': repo}

index = PackageIndex(
repo,
ca_cert=options.get('ca_cert', self.plugin_config.get('ca-cert')),
client_cert=options.get('client_cert', self.plugin_config.get('client-cert')),
client_key=options.get('client_key', self.plugin_config.get('client-key')),
repo_config['url'],
ca_cert=options.get('ca_cert', repo_config.get('ca-cert')),
client_cert=options.get('client_cert', repo_config.get('client-cert')),
client_key=options.get('client_key', repo_config.get('client-key')),
)

cached_user_file = CachedUserFile(self.cache_dir)
updated_user = None
if 'user' in options:
user = options['user']
else:
user = self.plugin_config.get('user', '')
user = repo_config.get('user', '')
if not user:
user = cached_user_file.get_user(repo)
if user is None:
Expand All @@ -66,7 +91,7 @@ def publish(self, artifacts: list, options: dict):
if 'auth' in options:
auth = options['auth']
else:
auth = self.plugin_config.get('auth', '')
auth = repo_config.get('auth', '')
if not auth:
import keyring

Expand Down
2 changes: 1 addition & 1 deletion tests/cli/config/test_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def test_reveal(hatch, config_file, helpers, default_cache_dir, default_data_dir
[projects]

[publish.index]
user = ""
repo = "main"
auth = "bar"

[template]
Expand Down
81 changes: 81 additions & 0 deletions tests/cli/publish/test_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,44 @@ def test_disabled(hatch, temp_dir, config_file):
assert result.output == 'Publisher is disabled: index\n'


def test_repo_invalid_type(hatch, temp_dir, config_file):
config_file.model.publish['index']['repos'] = {'dev': 9000}
config_file.save()

project_name = 'My.App'

with temp_dir.as_cwd():
result = hatch('new', project_name)
assert result.exit_code == 0, result.output

path = temp_dir / 'my-app'

with path.as_cwd():
result = hatch('publish', '--user', 'foo', '--auth', 'bar')

assert result.exit_code == 1, result.output
assert result.output == 'Hatch config field `publish.index.repos.dev` must be a string or a mapping\n'


def test_repo_missing_url(hatch, temp_dir, config_file):
config_file.model.publish['index']['repos'] = {'dev': {}}
config_file.save()

project_name = 'My.App'

with temp_dir.as_cwd():
result = hatch('new', project_name)
assert result.exit_code == 0, result.output

path = temp_dir / 'my-app'

with path.as_cwd():
result = hatch('publish', '--user', 'foo', '--auth', 'bar')

assert result.exit_code == 1, result.output
assert result.output == 'Hatch config field `publish.index.repos.dev` must define a `url` key\n'


def test_missing_user(hatch, temp_dir):
project_name = 'My.App'

Expand Down Expand Up @@ -226,6 +264,49 @@ def test_plugin_config(hatch, devpi, temp_dir_cache, helpers, published_project_
)


def test_plugin_config_repo_override(hatch, devpi, temp_dir_cache, helpers, published_project_name, config_file):
config_file.model.publish['index']['user'] = 'foo'
config_file.model.publish['index']['auth'] = 'bar'
config_file.model.publish['index']['ca-cert'] = 'cert'
config_file.model.publish['index']['repo'] = 'dev'
config_file.model.publish['index']['repos'] = {
'dev': {'url': devpi.repo, 'user': devpi.user, 'auth': devpi.auth, 'ca-cert': devpi.ca_cert},
}
config_file.save()

with temp_dir_cache.as_cwd():
result = hatch('new', published_project_name)
assert result.exit_code == 0, result.output

path = temp_dir_cache / published_project_name

with path.as_cwd():
del os.environ[PublishEnvVars.REPO]

current_version = timestamp_to_version(helpers.get_current_timestamp())
result = hatch('version', current_version)
assert result.exit_code == 0, result.output

result = hatch('build')
assert result.exit_code == 0, result.output

build_directory = path / 'dist'
artifacts = list(build_directory.iterdir())

result = hatch('publish')

assert result.exit_code == 0, result.output
assert result.output == helpers.dedent(
f"""
{artifacts[0].relative_to(path)} ... success
{artifacts[1].relative_to(path)} ... success

[{published_project_name}]
{devpi.repo}{published_project_name}/{current_version}/
"""
)


def test_prompt(hatch, devpi, temp_dir_cache, helpers, published_project_name, config_file):
config_file.model.publish['index']['ca-cert'] = devpi.ca_cert
config_file.model.publish['index']['repo'] = 'dev'
Expand Down
6 changes: 3 additions & 3 deletions tests/config/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_default(default_cache_dir, default_data_dir):
'cache': str(default_cache_dir),
},
'projects': {},
'publish': {'index': {'user': '', 'auth': ''}},
'publish': {'index': {'repo': 'main'}},
'template': {
'name': 'Foo Bar',
'email': 'foo@bar.baz',
Expand Down Expand Up @@ -722,8 +722,8 @@ class TestPublish:
def test_default(self):
config = RootConfig({})

assert config.publish == config.publish == {'index': {'user': '', 'auth': ''}}
assert config.raw_data == {'publish': {'index': {'user': '', 'auth': ''}}}
assert config.publish == config.publish == {'index': {'repo': 'main'}}
assert config.raw_data == {'publish': {'index': {'repo': 'main'}}}

def test_defined(self):
config = RootConfig({'publish': {'foo': {'username': '', 'password': ''}}})
Expand Down