• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pybuilder / pybuilder / 23886302068

02 Apr 2026 05:56AM UTC coverage: 82.885% (-0.08%) from 82.968%
23886302068

push

github

web-flow
Add --project-info (-i) CLI option for JSON project configuration dump [release] (#946)

## Summary

- Adds `pyb -i` / `pyb --project-info` that outputs the full project
configuration as pretty-printed JSON to stdout without running a build
- Runs plugin initializers to populate all properties but does not
execute any tasks or create build/test venvs
- Log messages go to stderr (via new `StdErrLogger` /
`ColoredStdErrLogger` classes) so stdout is always clean, parseable JSON
- Mutually exclusive with `-t`, `-T`, `--start-project`,
`--update-project`

### JSON output includes:
- Project metadata (name, version, authors, license, URLs, etc.)
- All build properties (built-in + plugin-defined, after initializers
run)
- Loaded plugins
- Runtime, build, plugin, and extras dependencies
- Available tasks with descriptions and dependency graphs
- Manifest files, package data, files to install

### Usage:
```bash
pyb -i 2>/dev/null | jq .project.name
pyb -i -E ci -P verbose=true 2>/dev/null | jq .properties
```

## Test plan

- [x] 678 unit tests pass (including new tests for option parsing,
stderr logging, serialization, JSON output)
- [x] 3 cram tests pass (help output updated, new project-info cram
test, existing no-build test)
- [x] `pyb -i | python -m json.tool` produces valid JSON
- [x] `pyb -i -X 2>log.txt` sends debug logs to stderr, JSON to stdout
- [x] `pyb -i -t` rejected as mutually exclusive

1426 of 1888 branches covered (75.53%)

Branch coverage included in aggregate %.

54 of 72 new or added lines in 1 file covered. (75.0%)

1 existing line in 1 file now uncovered.

5606 of 6596 relevant lines covered (84.99%)

33.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.33
/src/main/python/pybuilder/extern/__init__.py
1
#   -*- coding: utf-8 -*-
2
#
3
#   This file is part of PyBuilder
4
#
5
#   Copyright 2011-2020 PyBuilder Team
6
#
7
#   Licensed under the Apache License, Version 2.0 (the "License");
8
#   you may not use this file except in compliance with the License.
9
#   You may obtain a copy of the License at
10
#
11
#       http://www.apache.org/licenses/LICENSE-2.0
12
#
13
#   Unless required by applicable law or agreed to in writing, software
14
#   distributed under the License is distributed on an "AS IS" BASIS,
15
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
#   See the License for the specific language governing permissions and
17
#   limitations under the License.
18

19
import sys
40✔
20
from importlib import import_module
40✔
21
from importlib.abc import Loader, MetaPathFinder
40✔
22
from importlib.util import spec_from_loader
40✔
23

24
import pybuilder._vendor
40✔
25

26

27
class VendorImporter(Loader, MetaPathFinder):
40✔
28
    """
29
    A PEP 302 meta path importer for finding optionally-vendored
30
    or otherwise naturally-installed packages from root_name.
31
    """
32

33
    def __init__(self, root_name, vendored_names, vendor_pkg):
40✔
34
        self.root_name = root_name
40✔
35
        self.vendored_names = set(vendored_names)
40✔
36
        self.vendor_pkg = vendor_pkg
40✔
37
        self._in_flight_imports = set()
40✔
38

39
    @property
40✔
40
    def search_path(self):
40✔
41
        """
42
        Search first the vendor package then as a natural package.
43
        """
44
        yield self.vendor_pkg + "."
40✔
45

46
    def find_module(self, fullname, path=None):
40✔
47
        """
48
        Return self when fullname starts with root_name and the
49
        target module is one vendored through this importer.
50
        """
51
        root, base, target = fullname.partition(self.root_name + ".")
×
52
        if root == fullname and not base and not target:
×
53
            root = None
×
54
            target = fullname
×
55
        if root:
×
56
            return
×
57
        if not any(map(target.startswith, self.vendored_names)):
×
58
            return
×
59
        return self
×
60

61
    def load_module(self, fullname):
40✔
62
        """
63
        Iterate over the search path to locate and load fullname.
64
        """
65
        root, base, target = fullname.partition(self.root_name + ".")
40✔
66
        if root == fullname and not base and not target:
40!
67
            root = None
40✔
68
            target = fullname
40✔
69
        for prefix in self.search_path:
40!
70
            extant = prefix + target
40✔
71
            if extant not in self._in_flight_imports:
40✔
72
                self._in_flight_imports.add(extant)
40✔
73
                try:
40✔
74
                    mod = import_module(extant)
40✔
75
                finally:
76
                    self._in_flight_imports.remove(extant)
40✔
77
            if extant in sys.modules:
40!
78
                mod = sys.modules[extant]
40✔
79
                sys.modules[fullname] = mod
40✔
80
                return mod
40✔
81
        else:
82
            raise ImportError(
×
83
                "The '{target}' package is required; "
84
                "normally this is bundled with this package so if you get "
85
                "this warning, consult the packager of your "
86
                "distribution.".format(**locals())
87
            )
88

89
    def find_spec(self, fullname, path=None, target=None):
40✔
90
        """Return a module spec for vendored names."""
91
        return (
40✔
92
            spec_from_loader(fullname, self)
93
            if self._module_matches_namespace(fullname) else None
94
        )
95

96
    def _module_matches_namespace(self, fullname):
40✔
97
        """Figure out if the target module is vendored."""
98
        root, base, target = fullname.partition(self.root_name + '.')
40✔
99
        if root == fullname and not base and not target:
40!
100
            root = None
40✔
101
            target = fullname
40✔
102
        return not root and any(map(target.startswith, self.vendored_names))
40✔
103

104
    def _find_distributions(self, context):
40✔
105
        context.path.insert(0, pybuilder._vendor.__file__[:-len("__init__.py") - 1])
40✔
106
        return []
40✔
107

108
    # https://github.com/pybuilder/pybuilder/issues/807
109
    if sys.version_info[:2] == (3, 8):
40!
110
        def find_distributions(self, context):
×
111
            return iter(self._find_distributions(context))
×
112
    else:
113
        find_distributions = _find_distributions
40✔
114

115
    def install(self):
40✔
116
        """
117
        Install this importer into sys.meta_path if not already present.
118
        """
119
        if self not in sys.meta_path:
40!
120
            sys.meta_path.insert(0, self)
40✔
121

122
            for pkg in self.vendored_names:
40✔
123
                for p in list(sys.modules):
40✔
124
                    if p == pkg or p.startswith(pkg + "."):
40✔
UNCOV
125
                        sys.modules.pop(p, None)
16✔
126

127

128
VendorImporter(__name__, pybuilder._vendor.__names__, pybuilder._vendor.__package__).install()
40✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc