├── .github
├── CONTRIBUTING.md
├── dependabot.yml
├── release.yml
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── docs
├── _images
│ ├── iterm.png
│ └── uproot-browser-logo.png
└── make_logo.py
├── noxfile.py
├── pyproject.toml
├── src
└── uproot_browser
│ ├── __init__.py
│ ├── __main__.py
│ ├── _version.pyi
│ ├── exceptions.py
│ ├── plot.py
│ ├── plot_mpl.py
│ ├── py.typed
│ ├── tree.py
│ └── tui
│ ├── README.md
│ ├── __init__.py
│ ├── browser.css
│ ├── browser.py
│ ├── header.py
│ ├── help.py
│ ├── left_panel.py
│ └── right_panel.py
└── tests
├── test_printouts.py
└── test_tui.py
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See the [Scikit-HEP Developer introduction][skhep-dev-intro] for a
2 | detailed description of best practices for developing Scikit-HEP packages.
3 |
4 | [skhep-dev-intro]: https://scikit-hep.org/developer/intro
5 |
6 | # Quick development
7 |
8 | The fastest way to start with development is to use nox. If you don't have nox,
9 | you can use `pipx run nox` to run it without installing, or `pipx install nox`.
10 | If you don't have pipx (pip for applications), then you can install with with
11 | `pip install pipx` (the only case were installing an application with regular
12 | pip is reasonable). If you use macOS, then pipx and nox are both in brew, use
13 | `brew install pipx nox`.
14 |
15 | To use, run `nox`. This will lint and test using every installed version of
16 | Python on your system, skipping ones that are not installed. You can also run
17 | specific jobs:
18 |
19 | ```console
20 | $ nox -l # List all the defined sessions
21 | $ nox -s lint # Lint only
22 | $ nox -s tests # Run the tests
23 | $ nox -s build # Make an SDist and wheel
24 | ```
25 |
26 | Nox handles everything for you, including setting up an temporary virtual
27 | environment for each run.
28 |
29 | # Setting up a development environment manually
30 |
31 | You can set up a development environment by running:
32 |
33 | ```bash
34 | python3 -m venv .venv
35 | source ./.env/bin/activate
36 | pip install -U pip dependency-groups
37 | pip install -e. $(dependency-groups dev)
38 | ```
39 |
40 | If you use `uv`, you can do:
41 |
42 | ```bash
43 | uv sync
44 | ```
45 |
46 | instead.
47 |
48 | # Post setup
49 |
50 | You should prepare pre-commit, which will help you by checking that commits
51 | pass required checks:
52 |
53 | ```bash
54 | pip install pre-commit # or brew install pre-commit on macOS
55 | pre-commit install # Will install a pre-commit hook into the git repo
56 | ```
57 |
58 | You can also/alternatively run `pre-commit run` (changes only) or `pre-commit
59 | run --all-files` to check even without installing the hook.
60 |
61 | # Testing
62 |
63 | Use pytest to run the unit checks:
64 |
65 | ```bash
66 | pytest
67 | ```
68 |
69 | # Pre-commit
70 |
71 | This project uses pre-commit for all style checking. While you can run it with
72 | nox, this is such an important tool that it deserves to be installed on its
73 | own. Install pre-commit and run:
74 |
75 | ```bash
76 | pre-commit run -a
77 | ```
78 |
79 | to check all files.
80 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain dependencies for GitHub Actions
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "monthly"
8 | groups:
9 | actions:
10 | patterns:
11 | - "*"
12 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | authors:
4 | - dependabot
5 | - pre-commit-ci
6 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types:
7 | - published
8 |
9 | concurrency:
10 | group: ${ github.workflow }-${ github.ref }
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | dist:
15 | name: Distribution build
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: hynek/build-and-inspect-python-package@v2
21 |
22 |
23 | publish:
24 | name: Publish
25 | environment: pypi
26 | permissions:
27 | id-token: write
28 | attestations: write
29 | needs: [dist]
30 | if: github.event_name == 'release' && github.event.action == 'published'
31 | runs-on: ubuntu-latest
32 |
33 | steps:
34 | - uses: actions/download-artifact@v4
35 | with:
36 | path: dist
37 | name: Packages
38 |
39 | - name: Generate artifact attestation for sdist and wheel
40 | uses: actions/attest-build-provenance@v2
41 | with:
42 | subject-path: "dist/uproot_browser-*"
43 |
44 | - uses: pypa/gh-action-pypi-publish@release/v1
45 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | push:
7 | branches:
8 | - main
9 |
10 | concurrency:
11 | group: ${ github.workflow }-${ github.ref }
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | pre-commit:
16 | name: Format
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: actions/setup-python@v5
21 | with:
22 | python-version: "3.x"
23 | - uses: pre-commit/action@v3.0.1
24 | - name: PyLint
25 | run: pipx run nox -s pylint
26 |
27 | checks:
28 | name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}
29 | runs-on: ${{ matrix.runs-on }}
30 | strategy:
31 | fail-fast: false
32 | matrix:
33 | python-version: ["3.9", "3.11"]
34 | runs-on: [ubuntu-latest, macos-13, windows-latest]
35 | include:
36 | - python-version: "3.13"
37 | runs-on: ubuntu-latest
38 | - python-version: "3.13"
39 | runs-on: macos-latest
40 | - python-version: pypy-3.10
41 | runs-on: ubuntu-latest
42 |
43 | steps:
44 | - uses: actions/checkout@v4
45 |
46 | - uses: actions/setup-python@v5
47 | with:
48 | python-version: ${{ matrix.python-version }}
49 | allow-prereleases: true
50 |
51 | - uses: astral-sh/setup-uv@v5
52 |
53 | - name: Install nox
54 | run: uv tool install nox
55 |
56 | - name: Test package
57 | run: nox -s tests
58 |
59 | - name: Test minimum versions
60 | if: matrix.python-version != 'pypy-3.10' && matrix.python-version != '3.13'
61 | run: nox -s minimums
62 |
63 | pass:
64 | if: always()
65 | needs: [pre-commit, checks]
66 | runs-on: ubuntu-latest
67 | steps:
68 | - name: Decide whether the needed jobs succeeded or failed
69 | uses: re-actors/alls-green@release/v1
70 | with:
71 | jobs: ${{ toJSON(needs) }}
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv*
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
140 | # setuptools_scm
141 | src/*/_version.py
142 |
143 | # Common debug file
144 | /uproot-Event.root
145 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v5.0.0
4 | hooks:
5 | - id: check-added-large-files
6 | - id: check-case-conflict
7 | - id: check-merge-conflict
8 | - id: check-symlinks
9 | - id: check-yaml
10 | - id: debug-statements
11 | - id: end-of-file-fixer
12 | - id: mixed-line-ending
13 | - id: requirements-txt-fixer
14 | - id: trailing-whitespace
15 |
16 | - repo: https://github.com/astral-sh/ruff-pre-commit
17 | rev: "v0.9.2"
18 | hooks:
19 | - id: ruff
20 | args: ["--fix", "--show-fixes"]
21 | - id: ruff-format
22 |
23 | - repo: https://github.com/pre-commit/mirrors-mypy
24 | rev: v1.14.1
25 | hooks:
26 | - id: mypy
27 | files: src
28 | additional_dependencies:
29 | - rich>=12
30 | - click
31 | - hist
32 | - numpy
33 | - textual>=0.86
34 |
35 | - repo: https://github.com/codespell-project/codespell
36 | rev: v2.3.0
37 | hooks:
38 | - id: codespell
39 | args: [-L, "hist,iterm"]
40 |
41 | - repo: local
42 | hooks:
43 | - id: disallow-caps
44 | name: Disallow improper capitalization
45 | language: pygrep
46 | entry: PyBind|Numpy|Cmake|CCache|Github|PyTest
47 | exclude: .pre-commit-config.yaml
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2021, Henry Schreiner
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # uproot-browser
4 |
5 | [![Actions Status][actions-badge]][actions-link]
6 | [![PyPI version][pypi-version]][pypi-link]
7 | [![PyPI platforms][pypi-platforms]][pypi-link]
8 | [![GitHub Discussion][github-discussions-badge]][github-discussions-link]
9 | [![Gitter][gitter-badge]][gitter-link]
10 | [![License][license-badge]][license-link]
11 | [![Scikit-HEP][sk-badge]](https://scikit-hep.org/)
12 | [![Conda-Forge][conda-badge]][conda-link]
13 |
14 | uproot-browser is a [plotext](https://github.com/piccolomo/plotext) based command line library in which the command line interface is provided by [Click](https://github.com/pallets/click). It is powered by [Hist](https://github.com/scikit-hep/hist) and it's TUI is put together by [Textual](https://github.com/Textualize/textual). Its aim is to enable a user to browse and look inside a ROOT file, completely via the terminal. It takes its inspiration from the [ROOT object browser](https://root.cern/doc/master/classTRootBrowser.html).
15 |
16 | ## Installation
17 |
18 | You can install this library from [PyPI](https://pypi.org/project/uproot-browser/) with `pip`:
19 |
20 | ```bash
21 | python3 -m pip install uproot-browser
22 | ```
23 |
24 | You can also use `pipx` to run the library without installing it:
25 |
26 | ```bash
27 | pipx run uproot-browser
28 | ```
29 |
30 | ## Features
31 |
32 | uproot-browser currently provides the following features (get help with `-h` or `--help`, view the current version with `--version`):
33 |
34 | - `browse` can be used to display a TUI (text user interface), acts as default if no subcommand specified.
35 | - `plot` can be used to display a plot.
36 | - `tree` can be used to display a tree.
37 |
38 |
39 | ## Examples
40 |
41 | This example uses data from the [scikit-hep-testdata](https://github.com/scikit-hep/scikit-hep-testdata) package. It is placed in the same directory as the uproot-browser repository.
42 |
43 | **`browse` command:**
44 |
45 | ```bash
46 | uproot-browser browse ../scikit-hep-testdata/src/skhep_testdata/data/uproot-Event.root
47 | ```
48 |
49 | 
50 |
51 | **`plot` command:**
52 |
53 | ```bash
54 | uproot-browser plot ../scikit-hep-testdata/src/skhep_testdata/data/uproot-Event.root:hstat
55 | hstat -- Entries: 1000
56 | ┌───────────────────────────────────────────────────────────────┐
57 | 18.0┤▐▌ │
58 | │▐▌ ▗▖ ▄│
59 | 15.6┤▐▌▗▖ ▐▌ █│
60 | │███▌ █ █ ▐▌ ▐█│
61 | 13.1┤████▟▌ ▗▖ ▗▖ █▌▗▖ ▐▌ ▄ █▌ ▄ ▟▌█ ▗▄▐▙▗▖ ▐▌▐█│
62 | 10.6┤█████▌ ▐▌ ▐▙▖ █▌▐▌ ▐▙ █▄ █▙ █ █▌█ ▐█▟█▐▌ ▗▄▟▌▐█│
63 | │█████▌ █▌▐█▌ ████▌█▌▐█ ▐█▐▌ ▐▌ ███▐██ ▐█ ▐████▐███▐▌ ▐███▌▐█│
64 | 8.2┤█████▌▐█▌▐█▌ █████▌██▐█ ██▐█ ▐▌▐████▐███▌▐█ █████▐███▐██▐████▐█│
65 | │████████▙██▌█████████▟█▖████▖▟██████▟██████▖████████████▟██████│
66 | 5.8┤███████████▙███████████▙████▌██████████████▌███████████████████│
67 | │████████████████████████████▌██████████████████████████████████│
68 | 3.3┤███████████████████████████████████████████████████████████████│
69 | └┬───────────────┬──────────────┬───────────────┬──────────────┬┘
70 | 0.00 0.25 0.50 0.75 1.00
71 | [x] xaxis
72 | ```
73 |
74 |
75 | If you're on macOS and using iTerm2, click here:
76 |
77 | You can get an iterm plot, the required dependencies can be installed via:
78 |
79 | ```bash
80 | python3 -m pip install uproot-browser[iterm]
81 | ```
82 |
83 | Or can be run via `pipx` without installing:
84 |
85 | ```bash
86 | pipx run uproot-browser[iterm]
87 | ```
88 |
89 | Adding the argument `--iterm` gives us the plot:
90 |
91 | ```bash
92 | uproot-browser plot ../scikit-hep-testdata/src/skhep_testdata/data/uproot-Event.root:hstat --iterm
93 | ```
94 |
95 |
96 |
97 |
98 |
99 | **`tree` command:**
100 |
101 | ```bash
102 | uproot-browser tree ../scikit-hep-testdata/src/skhep_testdata/data/uproot-Event.root
103 | 📁 uproot-Event.root
104 | ┣━━ ❓ TProcessID
105 | ┣━━ 🌴 T (1000)
106 | ┃ ┗━━ 🌿 event Event
107 | ┃ ┣━━ 🌿 TObject (group of fUniqueID:uint32_t, fBits:uint32_t)
108 | ┃ ┃ ┣━━ 🍁 fBits uint32_t
109 | ┃ ┃ ┗━━ 🍁 fUniqueID uint32_t
110 | ┃ ┣━━ 🍁 fClosestDistance unknown[]
111 | ┃ ┣━━ 🍁 fEventName char*
112 | ┃ ┣━━ 🌿 fEvtHdr EventHeader
113 | ┃ ┃ ┣━━ 🍁 fEvtHdr.fDate int32_t
114 | ┃ ┃ ┣━━ 🍁 fEvtHdr.fEvtNum int32_t
115 | ┃ ┃ ┗━━ 🍁 fEvtHdr.fRun int32_t
116 | ┃ ┣━━ 🍁 fFlag uint32_t
117 | ┃ ┣━━ 🍁 fH TH1F
118 | ┃ ┣━━ 🍁 fHighPt TRefArray*
119 | ┃ ┣━━ 🍁 fIsValid bool
120 | ┃ ┣━━ 🍁 fLastTrack TRef
121 | ┃ ┣━━ 🍁 fMatrix[4][4] float[4][4]
122 | ┃ ┣━━ 🍁 fMeasures[10] int32_t[10]
123 | ┃ ┣━━ 🍁 fMuons TRefArray*
124 | ┃ ┣━━ 🍁 fNseg int32_t
125 | ┃ ┣━━ 🍁 fNtrack int32_t
126 | ┃ ┣━━ 🍁 fNvertex uint32_t
127 | ┃ ┣━━ 🍁 fTemperature float
128 | ┃ ┣━━ 🌿 fTracks TClonesArray*
129 | ┃ ┃ ┣━━ 🍃 fTracks.fBits uint32_t[]
130 | ┃ ┃ ┣━━ 🍃 fTracks.fBx Float16_t[]
131 | ┃ ┃ ┣━━ 🍃 fTracks.fBy Float16_t[]
132 | ┃ ┃ ┣━━ 🍃 fTracks.fCharge Double32_t[]
133 | ┃ ┃ ┣━━ 🍃 fTracks.fMass2 Float16_t[]
134 | ┃ ┃ ┣━━ 🍃 fTracks.fMeanCharge float[]
135 | ┃ ┃ ┣━━ 🍃 fTracks.fNpoint int32_t[]
136 | ┃ ┃ ┣━━ 🍃 fTracks.fNsp uint32_t[]
137 | ┃ ┃ ┣━━ 🍁 fTracks.fPointValue unknown[][]
138 | ┃ ┃ ┣━━ 🍃 fTracks.fPx float[]
139 | ┃ ┃ ┣━━ 🍃 fTracks.fPy float[]
140 | ┃ ┃ ┣━━ 🍃 fTracks.fPz float[]
141 | ┃ ┃ ┣━━ 🍃 fTracks.fRandom float[]
142 | ┃ ┃ ┣━━ 🍃 fTracks.fTArray[3] float[][3]
143 | ┃ ┃ ┣━━ 🍁 fTracks.fTriggerBits.fAllBits uint8_t[][]
144 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fBits uint32_t[]
145 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fNbits uint32_t[]
146 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fNbytes uint32_t[]
147 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fUniqueID uint32_t[]
148 | ┃ ┃ ┣━━ 🍃 fTracks.fUniqueID uint32_t[]
149 | ┃ ┃ ┣━━ 🍃 fTracks.fValid int16_t[]
150 | ┃ ┃ ┣━━ 🍃 fTracks.fVertex[3] Double32_t[][3]
151 | ┃ ┃ ┣━━ 🍃 fTracks.fXfirst Float16_t[]
152 | ┃ ┃ ┣━━ 🍃 fTracks.fXlast Float16_t[]
153 | ┃ ┃ ┣━━ 🍃 fTracks.fYfirst Float16_t[]
154 | ┃ ┃ ┣━━ 🍃 fTracks.fYlast Float16_t[]
155 | ┃ ┃ ┣━━ 🍃 fTracks.fZfirst Float16_t[]
156 | ┃ ┃ ┗━━ 🍃 fTracks.fZlast Float16_t[]
157 | ┃ ┣━━ 🌿 fTriggerBits TBits
158 | ┃ ┃ ┣━━ 🌿 fTriggerBits.TObject (group of fTriggerBits.fUniqueID:uint32_t, fTriggerBits.fBits:uint32_t)
159 | ┃ ┃ ┃ ┣━━ 🍁 fTriggerBits.fBits uint32_t
160 | ┃ ┃ ┃ ┗━━ 🍁 fTriggerBits.fUniqueID uint32_t
161 | ┃ ┃ ┣━━ 🍃 fTriggerBits.fAllBits uint8_t[]
162 | ┃ ┃ ┣━━ 🍁 fTriggerBits.fNbits uint32_t
163 | ┃ ┃ ┗━━ 🍁 fTriggerBits.fNbytes uint32_t
164 | ┃ ┣━━ 🍁 fType[20] int8_t[20]
165 | ┃ ┗━━ 🍁 fWebHistogram TRef
166 | ┣━━ 📊 hstat TH1F (100)
167 | ┗━━ 📊 htime TH1F (10)
168 | ```
169 |
170 | ## Development
171 |
172 | [![pre-commit.ci status][pre-commit-badge]][pre-commit-link]
173 |
174 | See [CONTRIBUTING.md](https://github.com/scikit-hep/uproot-browser/blob/main/.github/CONTRIBUTING.md) for details on how to set up a development environment.
175 |
176 | [actions-badge]: https://github.com/scikit-hep/uproot-browser/workflows/CI/badge.svg
177 | [actions-link]: https://github.com/scikit-hep/uproot-browser/actions
178 | [conda-badge]: https://img.shields.io/conda/vn/conda-forge/uproot-browser
179 | [conda-link]: https://github.com/conda-forge/uproot-browser-feedstock
180 | [github-discussions-badge]: https://img.shields.io/static/v1?label=Discussions&message=Ask&color=blue&logo=github
181 | [github-discussions-link]: https://github.com/scikit-hep/uproot-browser/discussions
182 | [gitter-badge]: https://badges.gitter.im/Scikit-HEP/community.svg
183 | [gitter-link]: https://gitter.im/Scikit-HEP/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge
184 | [license-badge]: https://img.shields.io/badge/License-BSD_3--Clause-blue.svg
185 | [license-link]: https://opensource.org/licenses/BSD-3-Clause
186 | [pypi-link]: https://pypi.org/project/uproot-browser/
187 | [pypi-platforms]: https://img.shields.io/pypi/pyversions/uproot-browser
188 | [pypi-version]: https://badge.fury.io/py/uproot-browser.svg
189 | [sk-badge]: https://scikit-hep.org/assets/images/Scikit--HEP-Project-blue.svg
190 | [pre-commit-badge]: https://results.pre-commit.ci/badge/github/scikit-hep/uproot-browser/main.svg
191 | [pre-commit-link]: https://results.pre-commit.ci/repo/github/scikit-hep/uproot-browser
192 |
--------------------------------------------------------------------------------
/docs/_images/iterm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scikit-hep/uproot-browser/055535039114db16ba640b73f673e4df4adf0ef9/docs/_images/iterm.png
--------------------------------------------------------------------------------
/docs/_images/uproot-browser-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scikit-hep/uproot-browser/055535039114db16ba640b73f673e4df4adf0ef9/docs/_images/uproot-browser-logo.png
--------------------------------------------------------------------------------
/docs/make_logo.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import PIL.Image
4 | import PIL.ImageDraw
5 | import PIL.ImageFilter
6 | import PIL.ImageFont
7 |
8 | LOGO = """\
9 | ┬ ┬┌─┐┬─┐┌─┐┌─┐┌┬┐5 ┌┐ ┬─┐┌─┐┬ ┬┌─┐┌─┐┬─┐
10 | │ │├─┘├┬┘│ ││ │ │───├┴┐├┬┘│ ││││└─┐├┤ ├┬┘
11 | └─┘┴ ┴└─└─┘└─┘ ┴ └─┘┴└─└─┘└┴┘└─┘└─┘┴└─
12 | """
13 |
14 | image = PIL.Image.new("RGBA", (810, 120), color=(0, 0, 0, 0))
15 |
16 | draw = PIL.ImageDraw.Draw(image)
17 |
18 | font = PIL.ImageFont.truetype("Sauce Code Pro Medium Nerd Font Complete.ttf", 32)
19 |
20 | draw.text((10, 0), LOGO, font=font, fill=(128, 128, 128, 255))
21 |
22 | image.save("docs/_images/uproot-browser-logo.png")
23 |
--------------------------------------------------------------------------------
/noxfile.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import nox
4 |
5 | nox.needs_version = ">=2024.4.15"
6 | nox.options.default_venv_backend = "uv|virtualenv"
7 |
8 |
9 | def dep_group(group: str) -> list[str]:
10 | return nox.project.load_toml("pyproject.toml")["dependency-groups"][group] # type: ignore[no-any-return]
11 |
12 |
13 | @nox.session(reuse_venv=True)
14 | def lint(session: nox.Session) -> None:
15 | """
16 | Run the linter.
17 | """
18 | session.install("pre-commit")
19 | session.run("pre-commit", "run", "--all-files", *session.posargs)
20 |
21 |
22 | @nox.session
23 | def pylint(session: nox.Session) -> None:
24 | """
25 | Run pylint.
26 | """
27 |
28 | session.install("-e.", "pylint", "matplotlib")
29 | session.run("pylint", "src", *session.posargs)
30 |
31 |
32 | @nox.session
33 | def tests(session: nox.Session) -> None:
34 | """
35 | Run the unit and regular tests.
36 | """
37 | session.install("-e.", *dep_group("test"))
38 | session.run("pytest", *session.posargs)
39 |
40 |
41 | @nox.session(venv_backend="uv")
42 | def minimums(session: nox.Session) -> None:
43 | """
44 | Run the unit and regular tests.
45 | """
46 | session.install(
47 | "-e.", *dep_group("test"), "--resolution=lowest-direct", "--only-binary=:all:"
48 | )
49 | session.run("pytest", *session.posargs)
50 |
51 |
52 | @nox.session(default=False)
53 | def run(session: nox.Session) -> None:
54 | """
55 | Install and run.
56 | """
57 | session.install("-e.", "--compile")
58 | session.run("uproot-browser", *session.posargs)
59 |
60 |
61 | @nox.session(reuse_venv=True, default=False)
62 | def build(session: nox.Session) -> None:
63 | """
64 | Build an SDist and wheel.
65 | """
66 |
67 | session.install("build")
68 | session.run("python", "-m", "build")
69 |
70 |
71 | @nox.session(default=False)
72 | def make_logo(session: nox.Session) -> None:
73 | """
74 | Rerender the logo.
75 | """
76 |
77 | session.install("pillow")
78 | session.run("python", "docs/make_logo.py")
79 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling", "hatch-vcs"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "uproot_browser"
7 | authors = [
8 | { name = "Henry Schreiner", email = "henryfs@princeton.edu" },
9 | ]
10 | maintainers = [
11 | { name = "The Scikit-HEP admins", email = "scikit-hep-admins@googlegroups.com" },
12 | ]
13 |
14 | description = "Tools to inspect ROOT files with uproot"
15 | readme = "README.md"
16 |
17 | requires-python = ">=3.9"
18 |
19 | classifiers = [
20 | "License :: OSI Approved :: BSD License",
21 | "Topic :: Scientific/Engineering",
22 | "Intended Audience :: Science/Research",
23 | "Intended Audience :: Developers",
24 | "Operating System :: OS Independent",
25 | "License :: OSI Approved :: BSD License",
26 | "Programming Language :: Python",
27 | "Programming Language :: Python :: 3",
28 | "Programming Language :: Python :: 3.9",
29 | "Programming Language :: Python :: 3.10",
30 | "Programming Language :: Python :: 3.11",
31 | "Programming Language :: Python :: 3.12",
32 | "Programming Language :: Python :: 3.13",
33 | "Development Status :: 4 - Beta",
34 | "Typing :: Typed",
35 | ]
36 |
37 | dynamic = ["version"]
38 | dependencies = [
39 | 'awkward >=2',
40 | 'click >=8',
41 | 'click-default-group >=1.2',
42 | 'hist >=2.4',
43 | 'lz4>=2',
44 | 'numpy >=1.18',
45 | 'plotext >=5.2.8',
46 | 'rich >=13.3.3',
47 | 'textual >=0.86.0',
48 | 'uproot >=5',
49 | ]
50 |
51 | [project.optional-dependencies]
52 | iterm = [
53 | "matplotlib",
54 | "itermplot==0.5",
55 | "mplhep",
56 | ]
57 |
58 | [project.urls]
59 | homepage = "https://github.com/scikit-hep/uproot-browser"
60 | repository = "https://github.com/scikit-hep/uproot-browser"
61 |
62 | [project.scripts]
63 | uproot-browser = "uproot_browser.__main__:main"
64 |
65 | [dependency-groups]
66 | test = [
67 | "pytest >=8",
68 | "pytest-asyncio >=0.24",
69 | "scikit-hep-testdata >=0.4.10",
70 | ]
71 | dev = [
72 | "ipython >=6",
73 | "textual-dev",
74 | {include-group = "test"},
75 | ]
76 |
77 | [tool.hatch]
78 | version.source = "vcs"
79 | build.hooks.vcs.version-file = "src/uproot_browser/_version.py"
80 |
81 | [tool.pytest.ini_options]
82 | minversion = "8.0"
83 | addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
84 | xfail_strict = true
85 | filterwarnings = [
86 | "error",
87 | "ignore:can't resolve package from __spec__ or __package__, falling back on __name__ and __path__:ImportWarning", # PyPy NumPy
88 | ]
89 | log_cli_level = "info"
90 | testpaths = ["tests"]
91 | asyncio_mode = "auto"
92 | asyncio_default_fixture_loop_scope = "function"
93 |
94 |
95 | [tool.mypy]
96 | files = "src"
97 | python_version = "3.9"
98 | warn_unused_configs = true
99 | strict = true
100 |
101 | [[tool.mypy.overrides]]
102 | module = ["plotext.*", "awkward.*", "uproot.*", "matplotlib.*"]
103 | ignore_missing_imports = true
104 |
105 |
106 | [tool.pylint]
107 | master.py-version = "3.9"
108 | master.jobs = "0"
109 | reports.output-format = "colorized"
110 | similarities.ignore-imports = "yes"
111 | messages_control.enable = [
112 | "useless-suppression",
113 | ]
114 | messages_control.disable = [
115 | "broad-except",
116 | "design",
117 | "invalid-name",
118 | "line-too-long",
119 | "missing-class-docstring",
120 | "missing-function-docstring",
121 | "missing-module-docstring",
122 | "duplicate-code",
123 | "unused-argument", # Handled by Ruff
124 | "wrong-import-position", # Handled by Ruff
125 | ]
126 |
127 |
128 | [tool.ruff.lint]
129 | extend-select = [
130 | "B", # flake8-bugbear
131 | "I", # isort
132 | "ARG", # flake8-unused-arguments
133 | "C4", # flake8-comprehensions
134 | "EM", # flake8-errmsg
135 | "ICN", # flake8-import-conventions
136 | "ISC", # flake8-implicit-str-concat
137 | "PGH", # pygrep-hooks
138 | "PIE", # flake8-pie
139 | "PL", # pylint
140 | "PT", # flake8-pytest-style
141 | "PTH", # flake8-use-pathlib
142 | "RET", # flake8-return
143 | "RUF", # Ruff-specific
144 | "SIM", # flake8-simplify
145 | "T20", # flake8-print
146 | "UP", # pyupgrade
147 | "YTT", # flake8-2020
148 | ]
149 | ignore = [
150 | "E501",
151 | "E722",
152 | "RUF001", # Unicode chars
153 | "PLR",
154 | "ISC001", # Conflicts with formatter
155 | ]
156 | unfixable = [
157 | "SIM118", # Dict .keys() removal (uproot)
158 | ]
159 |
160 | [tool.ruff.lint.per-file-ignores]
161 | "noxfile.py" = ["T20"]
162 | "tests/*" = ["T20"]
163 | "src/uproot_browser/tree.py" = ["UP006"]
164 |
--------------------------------------------------------------------------------
/src/uproot_browser/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This is uproot-browser. There is no user accessible API; only a terminal
3 | interface is provided currently.
4 | """
5 |
6 | from __future__ import annotations
7 |
8 | from ._version import version as __version__
9 |
10 | __all__ = ("__version__",)
11 |
--------------------------------------------------------------------------------
/src/uproot_browser/__main__.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the click-powered CLI.
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | import functools
8 | import os
9 | import typing
10 | from typing import Any, Callable
11 |
12 | import click
13 | import uproot
14 |
15 | from ._version import version as __version__
16 |
17 | CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
18 |
19 | VERSION = __version__
20 |
21 | if typing.TYPE_CHECKING:
22 | DefaultGroup = click.Group
23 | else:
24 | from click_default_group import DefaultGroup
25 |
26 |
27 | @click.group(context_settings=CONTEXT_SETTINGS, cls=DefaultGroup, default="browse")
28 | @click.version_option(version=VERSION)
29 | def main() -> None:
30 | """
31 | Must provide a subcommand.
32 | """
33 |
34 |
35 | @main.command()
36 | @click.argument("filename")
37 | def tree(filename: str) -> None:
38 | """
39 | Display a tree.
40 | """
41 | import uproot_browser.tree # pylint: disable=import-outside-toplevel
42 |
43 | uproot_browser.tree.print_tree(filename)
44 |
45 |
46 | def intercept(func: Callable[..., Any], *names: str) -> Callable[..., Any]:
47 | """
48 | Intercept function arguments and remove them
49 | """
50 |
51 | @functools.wraps(func)
52 | def new_func(*args: Any, **kwargs: Any) -> Any:
53 | for name in names:
54 | kwargs.pop(name)
55 | return func(*args, **kwargs)
56 |
57 | return new_func
58 |
59 |
60 | @main.command()
61 | @click.argument("filename")
62 | @click.option(
63 | "--iterm", is_flag=True, help="Display an iTerm plot (requires [iterm] extra)."
64 | )
65 | def plot(filename: str, iterm: bool) -> None:
66 | """
67 | Display a plot.
68 | """
69 | if iterm:
70 | os.environ.setdefault("MPLBACKEND", r"module://itermplot")
71 |
72 | import matplotlib.pyplot as plt # pylint: disable=import-outside-toplevel
73 |
74 | import uproot_browser.plot_mpl # pylint: disable=import-outside-toplevel
75 | else:
76 | import uproot_browser.plot # pylint: disable=import-outside-toplevel
77 |
78 | item = uproot.open(filename)
79 |
80 | if iterm:
81 | uproot_browser.plot_mpl.plot(item)
82 | if plt.get_backend() == r"module://itermplot":
83 | fm = plt.get_current_fig_manager()
84 | canvas = fm.canvas
85 | canvas.__class__.print_figure = intercept(
86 | canvas.__class__.print_figure, "facecolor", "edgecolor"
87 | )
88 |
89 | plt.show()
90 | else:
91 | uproot_browser.plot.clf()
92 | uproot_browser.plot.plot(item)
93 | uproot_browser.plot.show()
94 |
95 |
96 | @main.command()
97 | @click.argument("filename")
98 | def browse(filename: str) -> None:
99 | """
100 | Display a TUI.
101 | """
102 | import uproot_browser.tui.browser # pylint: disable=import-outside-toplevel
103 |
104 | app = uproot_browser.tui.browser.Browser(
105 | path=filename,
106 | )
107 |
108 | app.run()
109 |
110 |
111 | if __name__ == "__main__":
112 | main()
113 |
--------------------------------------------------------------------------------
/src/uproot_browser/_version.pyi:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | version: str
4 | version_tuple: tuple[int, int, int] | tuple[int, int, int, str, str]
5 |
--------------------------------------------------------------------------------
/src/uproot_browser/exceptions.py:
--------------------------------------------------------------------------------
1 | """Custom exceptions for uproot-browser"""
2 |
3 | from __future__ import annotations
4 |
5 |
6 | class EmptyTreeError(ValueError):
7 | pass
8 |
--------------------------------------------------------------------------------
/src/uproot_browser/plot.py:
--------------------------------------------------------------------------------
1 | """
2 | Display tools for making plots via plotext.
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | import functools
8 | import math
9 | from typing import Any
10 |
11 | import awkward as ak
12 | import hist
13 | import numpy as np
14 | import plotext as plt
15 | import uproot
16 |
17 | from uproot_browser.exceptions import EmptyTreeError
18 |
19 |
20 | def clf() -> None:
21 | """
22 | Clear the plot.
23 | """
24 | plt.clf()
25 |
26 |
27 | def show() -> None:
28 | """
29 | Show the plot.
30 | """
31 | plt.show()
32 |
33 |
34 | def make_hist_title(item: Any, histogram: hist.Hist) -> str:
35 | inner_sum: float = np.sum(histogram.values())
36 | full_sum: float = np.sum(histogram.values(flow=True))
37 |
38 | if math.isclose(inner_sum, full_sum):
39 | return f"{item.name} -- Entries: {inner_sum:g}"
40 |
41 | return f"{item.name} -- Entries: {inner_sum:g} ({full_sum:g} with flow)"
42 |
43 |
44 | @functools.singledispatch
45 | def plot(tree: Any) -> None: # noqa: ARG001
46 | """
47 | Implement this for each type of plottable.
48 | """
49 | msg = "This object is not plottable yet"
50 | raise RuntimeError(msg)
51 |
52 |
53 | @plot.register
54 | def plot_branch(tree: uproot.TBranch) -> None:
55 | """
56 | Plot a single tree branch.
57 | """
58 | array = tree.array()
59 | values = ak.flatten(array) if array.ndim > 1 else array
60 | finite = values[np.isfinite(values)]
61 | if len(finite) < 1:
62 | msg = f"Branch {tree.name} is empty."
63 | raise EmptyTreeError(msg)
64 | histogram: hist.Hist = hist.numpy.histogram(finite, bins=100, histogram=hist.Hist)
65 | plt.bar(histogram.axes[0].centers, histogram.values().astype(float))
66 | plt.ylim(lower=0)
67 | plt.xticks(np.linspace(histogram.axes[0][0][0], histogram.axes[0][-1][-1], 5))
68 | plt.xlabel(histogram.axes[0].name)
69 | plt.title(make_hist_title(tree, histogram))
70 |
71 |
72 | @plot.register
73 | def plot_hist(tree: uproot.behaviors.TH1.Histogram) -> None:
74 | """
75 | Plot a 1-D Histogram.
76 | """
77 | histogram = hist.Hist(tree.to_hist())
78 | plt.bar(histogram.axes[0].centers, histogram.values().astype(float))
79 | plt.ylim(lower=0)
80 | plt.xticks(np.linspace(histogram.axes[0][0][0], histogram.axes[0][-1][-1], 5))
81 | plt.xlabel(histogram.axes[0].name)
82 | plt.title(make_hist_title(tree, histogram))
83 |
--------------------------------------------------------------------------------
/src/uproot_browser/plot_mpl.py:
--------------------------------------------------------------------------------
1 | """
2 | Display tools for making plots via plotext.
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | import functools
8 | from typing import Any
9 |
10 | import awkward as ak
11 | import hist
12 | import matplotlib.pyplot as plt
13 | import uproot
14 | import uproot.behaviors.TH1
15 |
16 | import uproot_browser.plot
17 |
18 |
19 | @functools.singledispatch
20 | def plot(tree: Any) -> None: # noqa: ARG001
21 | """
22 | Implement this for each type of plottable.
23 | """
24 | msg = "This object is not plottable yet"
25 | raise RuntimeError(msg)
26 |
27 |
28 | @plot.register
29 | def plot_branch(tree: uproot.TBranch) -> None:
30 | """
31 | Plot a single tree branch.
32 | """
33 | array = tree.array()
34 | histogram: hist.Hist = hist.numpy.histogram(
35 | ak.flatten(array) if array.ndim > 1 else array, bins=50, histogram=hist.Hist
36 | )
37 | histogram.plot()
38 | plt.title(uproot_browser.plot.make_hist_title(tree, histogram))
39 |
40 |
41 | @plot.register
42 | def plot_hist(tree: uproot.behaviors.TH1.Histogram) -> None:
43 | """
44 | Plot a 1-D Histogram.
45 | """
46 | histogram = hist.Hist(tree.to_hist())
47 | histogram.plot()
48 | plt.title(uproot_browser.plot.make_hist_title(tree, histogram))
49 |
--------------------------------------------------------------------------------
/src/uproot_browser/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scikit-hep/uproot-browser/055535039114db16ba640b73f673e4df4adf0ef9/src/uproot_browser/py.typed
--------------------------------------------------------------------------------
/src/uproot_browser/tree.py:
--------------------------------------------------------------------------------
1 | """
2 | Display tools for TTrees.
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | import dataclasses
8 | import functools
9 | from pathlib import Path
10 | from typing import Any, TypedDict
11 |
12 | import uproot
13 | import uproot.reading
14 | from rich.console import Console
15 | from rich.markup import escape
16 | from rich.text import Text
17 | from rich.tree import Tree
18 |
19 | console = Console()
20 |
21 | __all__ = (
22 | "MetaDict",
23 | "UprootEntry",
24 | "console",
25 | "make_tree",
26 | "print_tree",
27 | "process_item",
28 | )
29 |
30 |
31 | def __dir__() -> tuple[str, ...]:
32 | return __all__
33 |
34 |
35 | class MetaDictRequired(TypedDict, total=True):
36 | label_text: Text
37 | label_icon: str
38 |
39 |
40 | class MetaDict(MetaDictRequired, total=False):
41 | guide_style: str
42 |
43 |
44 | @dataclasses.dataclass
45 | class UprootEntry:
46 | path: str
47 | item: Any
48 |
49 | @property
50 | def is_dir(self) -> bool:
51 | if isinstance(self.item, uproot.reading.ReadOnlyDirectory):
52 | return True
53 | if isinstance(self.item, uproot.behaviors.TBranch.HasBranches):
54 | return len(self.item.branches) > 0
55 | return False
56 |
57 | def meta(self) -> MetaDict:
58 | return process_item(self.item)
59 |
60 | def label(self) -> Text:
61 | meta = self.meta()
62 | return Text.assemble(meta["label_icon"], meta["label_text"])
63 |
64 | def tree_args(self) -> dict[str, Any]:
65 | d: dict[str, Text | str] = {"label": self.label()}
66 | if "guide_style" in self.meta():
67 | d["guide_style"] = self.meta()["guide_style"]
68 | return d
69 |
70 | @property
71 | def children(self) -> list[UprootEntry]:
72 | if not self.is_dir:
73 | return []
74 | if isinstance(self.item, uproot.reading.ReadOnlyDirectory):
75 | items = {
76 | key.split(";")[0]
77 | for key in self.item.keys() # noqa: SIM118
78 | if "/" not in key
79 | }
80 | elif isinstance(self.item, uproot.behaviors.TBranch.HasBranches):
81 | items = {item.name for item in self.item.branches}
82 | else:
83 | items = {obj.name.split(";")[0] for obj in self.item.branches}
84 | return [
85 | UprootEntry(f"{self.path}/{key}", self.item[key]) for key in sorted(items)
86 | ]
87 |
88 |
89 | def make_tree(node: UprootEntry, *, tree: Tree | None = None) -> Tree:
90 | """
91 | Given an object, build a rich.tree.Tree output.
92 | """
93 |
94 | tree = Tree(**node.tree_args()) if tree is None else tree.add(**node.tree_args())
95 |
96 | for child in node.children:
97 | make_tree(child, tree=tree)
98 |
99 | return tree
100 |
101 |
102 | @functools.singledispatch
103 | def process_item(uproot_object: Any) -> MetaDict:
104 | """
105 | Given an unknown object, return a rich.tree.Tree output. Specialize for known objects.
106 | """
107 | name = getattr(uproot_object, "name", "")
108 | classname = getattr(uproot_object, "classname", uproot_object.__class__.__name__)
109 | label_text = Text.assemble(
110 | (f"{name} ", "bold"),
111 | (classname, "italic"),
112 | )
113 | return MetaDict(label_icon="❓ ", label_text=label_text)
114 |
115 |
116 | @process_item.register
117 | def _process_item_tfile(
118 | uproot_object: uproot.reading.ReadOnlyDirectory,
119 | ) -> MetaDict:
120 | """
121 | Given an TFile, return a rich.tree.Tree output.
122 | """
123 | path = Path(uproot_object.file_path)
124 |
125 | if uproot_object.path:
126 | # path is to a TDirectory on tree
127 | path_name = escape(uproot_object.path[0])
128 | link_text = f"file://{path}:/{path_name}"
129 | else:
130 | # path is the top of the tree: the file
131 | path_name = escape(path.name)
132 | link_text = f"file://{path}"
133 |
134 | label_text = Text.from_markup(f"[link {link_text}]{path_name}")
135 |
136 | return MetaDict(
137 | label_icon="📁 ",
138 | label_text=label_text,
139 | guide_style="bold bright_blue",
140 | )
141 |
142 |
143 | @process_item.register
144 | def _process_item_ttree(uproot_object: uproot.TTree) -> MetaDict:
145 | """
146 | Given an tree, return a rich.tree.Tree output.
147 | """
148 | label_text = Text.assemble(
149 | (f"{uproot_object.name} ", "bold"),
150 | f"({uproot_object.num_entries:g})",
151 | )
152 |
153 | return MetaDict(
154 | label_icon="🌴 ",
155 | label_text=label_text,
156 | guide_style="bold bright_green",
157 | )
158 |
159 |
160 | @process_item.register
161 | def _process_item_tbranch(uproot_object: uproot.TBranch) -> MetaDict:
162 | """
163 | Given an branch, return a rich.tree.Tree output.
164 | """
165 |
166 | jagged = isinstance(
167 | uproot_object.interpretation, uproot.interpretation.jagged.AsJagged
168 | )
169 | icon = "🍃 " if jagged else "🍁 "
170 |
171 | if len(uproot_object.branches):
172 | icon = "🌿 "
173 |
174 | label_text = Text.assemble(
175 | (f"{uproot_object.name} ", "bold"),
176 | (f"{uproot_object.typename}", "italic"),
177 | )
178 |
179 | return MetaDict(
180 | label_icon=icon,
181 | label_text=label_text,
182 | guide_style="bold bright_green",
183 | )
184 |
185 |
186 | @process_item.register
187 | def _process_item_th(uproot_object: uproot.behaviors.TH1.Histogram) -> MetaDict:
188 | """
189 | Given an histogram, return a rich.tree.Tree output.
190 | """
191 | icon = "📊 " if uproot_object.kind == "COUNT" else "📈 "
192 | sizes = " × ".join(f"{len(ax)}" for ax in uproot_object.axes)
193 |
194 | label_text = Text.assemble(
195 | (f"{uproot_object.name} ", "bold"),
196 | (f"{uproot_object.classname} ", "italic"),
197 | f"({sizes})",
198 | )
199 | return MetaDict(
200 | label_icon=icon,
201 | label_text=label_text,
202 | )
203 |
204 |
205 | # pylint: disable-next=redefined-outer-name
206 | def print_tree(entry: str, *, console: Console = console) -> None:
207 | """
208 | Prints a tree given a specification string. Currently, that must be a
209 | single filename. Colons are not allowed currently in the filename.
210 | """
211 |
212 | upfile = uproot.open(entry)
213 | tree = make_tree(UprootEntry("/", upfile))
214 | console.print(tree)
215 |
--------------------------------------------------------------------------------
/src/uproot_browser/tui/README.md:
--------------------------------------------------------------------------------
1 | # Help
2 |
3 | Welcome to uproot-browser's help!
4 |
5 | ## Navigation
6 |
7 | Use the arrow keys to navigate the tree view. Press `enter` to select a
8 | something to plot. Press `spacebar` to open/close a directory or tree. You can
9 | also use the VIM keys: `j` to move down, `k` to move up, `l` to open a folder,
10 | and `h` to close a folder.
11 |
12 | ## Plotting
13 |
14 | Histograms, rectangular simple data (e.g. TTree's), and jagged arrays can
15 | be plotted. Click on an item or press `enter` to plot. If something can't
16 | be plotted, you'll see a scrollable error traceback. If you think it should
17 | be plottable, feel free to open an issue. 2D plots are not yet supported.
18 |
19 | ## Themes
20 |
21 | You can press `t` to toggle light and dark mode.
22 |
23 | ## Leaving
24 |
25 | You can press `q` to quit. You can also press `d` to quit with a dump of the
26 | current plot and how to get the object being plotted in Python uproot code.
27 |
28 | ## Exiting the help.
29 |
30 | You can press `esc` or `q` to exit this help screen. You can also use `tab` to
31 | switch between the panes in the help screen. You can use `F1` (or `?`) to access
32 | the help again.
33 |
34 | # Credits
35 |
36 | Uproot browser was created by Henry Schreiner and Aman Goel. It was rewritten
37 | by Elie Svoll and Jose Ayala Garcia to use the modern CSS-based Textual
38 | library.
39 |
40 | Uproot browser is made possible by the Scikit-HEP ecosystem, including uproot
41 | and awkward-array for data reading, hist and boost-histogram for computation.
42 | The TUI (terminal user interface) was built using the Textual library.
43 | Text-based plotting is provided by plotext.
44 |
--------------------------------------------------------------------------------
/src/uproot_browser/tui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scikit-hep/uproot-browser/055535039114db16ba640b73f673e4df4adf0ef9/src/uproot_browser/tui/__init__.py
--------------------------------------------------------------------------------
/src/uproot_browser/tui/browser.css:
--------------------------------------------------------------------------------
1 | Browser Widget {
2 | scrollbar-color: $secondary-darken-1;
3 | scrollbar-color-hover: $secondary-darken-3;
4 | scrollbar-color-active: $secondary-darken-3;
5 | scrollbar-background: $surface-darken-1;
6 | scrollbar-background-hover: $surface-darken-1;
7 | scrollbar-background-active: $surface-darken-1;
8 | scrollbar-size: 1 1;
9 | }
10 |
11 | #tree-view {
12 | display: none;
13 | overflow: auto;
14 | width: 25%;
15 | height: 100%;
16 | dock: left;
17 | padding-top: 1;
18 | }
19 |
20 | Tree > .tree--cursor {
21 | text-style: bold;
22 | }
23 |
24 | Browser.-show-tree #tree-view {
25 | display: block;
26 | max-width: 50%;
27 | }
28 |
29 | ContentSwitcher#main-view {
30 | height: 100%;
31 | }
32 |
33 | #logo {
34 | content-align: center middle;
35 | }
36 |
37 | #plot {
38 | overflow: auto;
39 | min-width: 100%;
40 | padding-left: 1;
41 | padding-right: 1;
42 | }
43 |
44 | #error{
45 | overflow: auto;
46 | min-width: 100%;
47 | }
48 |
49 | #empty {
50 | content-align: center middle;
51 | }
52 |
53 | Footer > .footer--highlight {
54 | background: $secondary-darken-2;
55 | }
56 |
57 | Footer > .footer--key {
58 | text-style: bold;
59 | background: $secondary-darken-3;
60 | }
61 |
62 | Footer > .footer--highlight-key {
63 | background: $secondary-darken-2;
64 | text-style: bold;
65 | }
66 |
67 | Footer {
68 | background: $secondary-lighten-2;
69 | }
70 |
71 | HelpScreen {
72 | align: center middle;
73 | }
74 |
75 | .dialog {
76 | padding: 0 1;
77 | width: 80%;
78 | height: 90%;
79 | border: thick $background 80%;
80 | background: $surface;
81 | }
82 |
83 | #help-buttons {
84 | align: right middle;
85 | height: 3;
86 | background: $surface-lighten-1;
87 | }
88 |
--------------------------------------------------------------------------------
/src/uproot_browser/tui/browser.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | if not __package__:
4 | __package__ = "uproot_browser.tui" # pylint: disable=redefined-builtin
5 |
6 | import contextlib
7 | import sys
8 | from typing import Any, ClassVar
9 |
10 | import plotext as plt
11 | import rich.syntax
12 | import textual.app
13 | import textual.binding
14 | import textual.containers
15 | import textual.events
16 | import textual.widgets
17 | from textual.reactive import var
18 |
19 | with contextlib.suppress(AttributeError):
20 | light_background = 0xF5, 0xF5, 0xF5
21 | # pylint: disable-next=protected-access
22 | plt._dict.themes["default"][0] = light_background
23 | # pylint: disable-next=protected-access
24 | plt._dict.themes["default"][1] = light_background
25 |
26 | dark_background = 0x1E, 0x1E, 0x1E
27 | dark_text = 0xFF, 0xA6, 0x2B
28 | # pylint: disable-next=protected-access
29 | plt._dict.themes["dark"][0] = dark_background
30 | # pylint: disable-next=protected-access
31 | plt._dict.themes["dark"][1] = dark_background
32 | # pylint: disable-next=protected-access
33 | plt._dict.themes["dark"][2] = dark_text
34 |
35 | from uproot_browser.exceptions import EmptyTreeError
36 |
37 | from .header import Header
38 | from .help import HelpScreen
39 | from .left_panel import UprootSelected, UprootTree
40 | from .right_panel import (
41 | EmptyWidget,
42 | Error,
43 | ErrorWidget,
44 | LogoWidget,
45 | Plotext,
46 | PlotWidget,
47 | make_plot,
48 | )
49 |
50 |
51 | class Browser(textual.app.App[object]):
52 | """A basic implementation of the uproot-browser TUI"""
53 |
54 | CSS_PATH = "browser.css"
55 | BINDINGS: ClassVar[
56 | list[textual.binding.Binding | tuple[str, str] | tuple[str, str, str]]
57 | ] = [
58 | textual.binding.Binding("b", "toggle_files", "Navbar"),
59 | textual.binding.Binding("q", "quit", "Quit"),
60 | textual.binding.Binding("d", "quit_with_dump", "Dump & Quit"),
61 | textual.binding.Binding("t", "toggle_theme", "Theme"),
62 | textual.binding.Binding("f1", "help", "Help"),
63 | textual.binding.Binding("?", "help", "Help", show=False),
64 | ]
65 |
66 | show_tree = var(True)
67 |
68 | def __init__(self, path: str, **kwargs: Any) -> None:
69 | self.path = path
70 | super().__init__(**kwargs)
71 |
72 | self.plot_widget = PlotWidget(id="plot")
73 | self.error_widget = ErrorWidget(id="error")
74 |
75 | def compose(self) -> textual.app.ComposeResult:
76 | """Compose our UI."""
77 | yield Header("uproot-browser")
78 | with textual.containers.Container():
79 | # left_panel
80 | yield UprootTree(self.path, id="tree-view")
81 | # right_panel
82 | yield textual.widgets.ContentSwitcher(
83 | LogoWidget(id="logo"),
84 | self.plot_widget,
85 | self.error_widget,
86 | EmptyWidget(id="empty"),
87 | id="main-view",
88 | initial="logo",
89 | )
90 | yield textual.widgets.Footer()
91 |
92 | def on_mount(self, _event: textual.events.Mount) -> None:
93 | self.query_one("#tree-view", UprootTree).focus()
94 |
95 | def watch_show_tree(self, show_tree: bool) -> None:
96 | """Called when show_tree is modified."""
97 | self.set_class(show_tree, "-show-tree")
98 |
99 | def action_help(self) -> None:
100 | self.push_screen(HelpScreen())
101 |
102 | def action_toggle_files(self) -> None:
103 | """Called in response to key binding."""
104 | self.show_tree = not self.show_tree
105 |
106 | def action_quit_with_dump(self) -> None:
107 | """Dump the current state of the application."""
108 |
109 | content_switcher = self.query_one("#main-view", textual.widgets.ContentSwitcher)
110 | err_widget = content_switcher.query_one("#error", ErrorWidget)
111 |
112 | msg = f'\nimport uproot\nuproot_file = uproot.open("{self.path}")'
113 |
114 | items: list[Plotext | Error] = []
115 | if content_switcher.current == "plot":
116 | assert self.plot_widget.item
117 | msg += (
118 | f'\nitem = uproot_file["{self.plot_widget.item.selection.lstrip("/")}"]'
119 | )
120 | items = [self.plot_widget.item]
121 | elif content_switcher.current == "error":
122 | assert err_widget.exc
123 | items = [err_widget.exc]
124 |
125 | dark = self.theme != "textual-light"
126 |
127 | theme = "ansi_dark" if dark else "ansi_light"
128 |
129 | results = rich.console.Group(
130 | *items,
131 | rich.syntax.Syntax(f"\n{msg}\n", "python", theme=theme),
132 | )
133 |
134 | self.exit(message=results)
135 |
136 | def action_toggle_theme(self) -> None:
137 | """An action to toggle dark mode."""
138 | dark = self.theme != "textual-light"
139 | theme = "textual-light" if dark else "textual-dark"
140 |
141 | if self.plot_widget.item:
142 | self.plot_widget.item.theme = "dark" if dark else "default"
143 | self.theme = theme
144 |
145 | def on_uproot_selected(self, message: UprootSelected) -> None:
146 | """A message sent by the tree when a file is clicked."""
147 |
148 | content_switcher = self.query_one("#main-view", textual.widgets.ContentSwitcher)
149 |
150 | try:
151 | dark = self.theme != "textual-light"
152 | theme = "dark" if dark else "default"
153 | make_plot(message.upfile[message.path], theme, 20)
154 | self.plot_widget.item = Plotext(message.upfile, message.path, theme)
155 | content_switcher.current = "plot"
156 |
157 | except EmptyTreeError:
158 | content_switcher.current = "empty"
159 |
160 | except Exception:
161 | exc = sys.exc_info()
162 | assert exc[1]
163 | self.error_widget.exc = Error(exc)
164 | content_switcher.current = "error"
165 |
166 |
167 | if __name__ in {"", "__main__"}:
168 | fname = "../scikit-hep-testdata/src/skhep_testdata/data/uproot-Event.root"
169 | app = Browser(path=fname)
170 | app.run()
171 |
--------------------------------------------------------------------------------
/src/uproot_browser/tui/header.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import typing
4 | from typing import Any
5 |
6 | import rich.text
7 | import textual.app
8 | import textual.reactive
9 | import textual.widget
10 |
11 | if typing.TYPE_CHECKING:
12 | from .browser import Browser
13 |
14 |
15 | class HeaderCloseIcon(textual.widget.Widget):
16 | DEFAULT_CSS = """
17 | HeaderCloseIcon {
18 | dock: left;
19 | padding: 0 1;
20 | width: 4;
21 | content-align: left middle;
22 | }
23 | HeaderCloseIcon:hover {
24 | background: $panel-lighten-2;
25 | }
26 | """
27 |
28 | def render(self) -> textual.app.RenderResult:
29 | return "❌"
30 |
31 | def on_click(self) -> None:
32 | self.app.exit()
33 |
34 |
35 | class HeaderHelpIcon(textual.widget.Widget):
36 | DEFAULT_CSS = """
37 | HeaderHelpIcon {
38 | dock: right;
39 | padding: 0 1;
40 | width: 4;
41 | content-align: right middle;
42 | }
43 | HeaderHelpIcon:hover {
44 | background: $panel-lighten-2;
45 | }
46 | """
47 |
48 | app: Browser
49 |
50 | def render(self) -> textual.app.RenderResult:
51 | return "❓"
52 |
53 | def on_click(self) -> None:
54 | self.app.action_help()
55 |
56 |
57 | class HeaderTitle(textual.widget.Widget):
58 | DEFAULT_CSS = """
59 | HeaderTitle {
60 | content-align: center middle;
61 | width: 100%;
62 | }
63 | """
64 |
65 | text = textual.reactive.Reactive("")
66 | sub_text = textual.reactive.Reactive("")
67 |
68 | def render(self) -> textual.app.RenderResult:
69 | text = rich.text.Text(self.text, no_wrap=True, overflow="ellipsis")
70 | if self.sub_text:
71 | text.append(" — ")
72 | text.append(self.sub_text, "dim")
73 | return text
74 |
75 |
76 | class Header(textual.widget.Widget):
77 | DEFAULT_CSS = """
78 | Header {
79 | dock: top;
80 | width: 100%;
81 | background: $foreground 5%;
82 | color: $text;
83 | height: 1;
84 | }
85 | """
86 |
87 | DEFAULT_CLASSES = ""
88 |
89 | def __init__(self, title: str, **kwargs: Any):
90 | super().__init__(**kwargs)
91 | self.title = title
92 |
93 | def compose(self) -> textual.app.ComposeResult:
94 | yield HeaderCloseIcon()
95 | yield HeaderTitle()
96 | yield HeaderHelpIcon()
97 |
98 | def on_mount(self) -> None:
99 | self.query_one(HeaderTitle).text = self.title
100 |
--------------------------------------------------------------------------------
/src/uproot_browser/tui/help.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import typing
4 | from importlib.resources import files
5 | from typing import ClassVar
6 |
7 | import textual.app
8 | import textual.binding
9 | import textual.containers
10 | import textual.screen
11 | import textual.widgets
12 |
13 | if typing.TYPE_CHECKING:
14 | from .browser import Browser
15 |
16 |
17 | class HelpScreen(textual.screen.ModalScreen[None]):
18 | BINDINGS: ClassVar[
19 | list[textual.binding.Binding | tuple[str, str] | tuple[str, str, str]]
20 | ] = [
21 | textual.binding.Binding("d", "", "Nothing", show=False),
22 | textual.binding.Binding("b", "", "Nothing", show=False),
23 | textual.binding.Binding("f1", "", "Nothing", show=False),
24 | textual.binding.Binding("q", "done", "Done", show=True),
25 | textual.binding.Binding("esc", "done", "Done", show=True),
26 | textual.binding.Binding("t", "toggle_theme", "Theme", show=True),
27 | ]
28 |
29 | app: Browser
30 |
31 | def compose(self) -> textual.app.ComposeResult:
32 | markdown = files("uproot_browser.tui").joinpath("README.md").read_text()
33 | with textual.containers.Container(id="help-dialog", classes="dialog"):
34 | yield textual.widgets.MarkdownViewer(markdown, id="help-text")
35 | with textual.containers.Container(id="help-buttons"):
36 | yield textual.widgets.Button("Done", variant="primary", id="help-done")
37 |
38 | def on_mount(self) -> None:
39 | self.query_one("#help-text", textual.widgets.MarkdownViewer).focus()
40 |
41 | def on_button_pressed(self, _event: textual.widgets.Button.Pressed) -> None:
42 | self.app.pop_screen()
43 |
44 | def action_done(self) -> None:
45 | self.app.pop_screen()
46 |
47 | def action_toggle_theme(self) -> None:
48 | self.app.action_toggle_theme()
49 |
--------------------------------------------------------------------------------
/src/uproot_browser/tui/left_panel.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 | from typing import Any, ClassVar
5 |
6 | import rich.panel
7 | import rich.repr
8 | import rich.text
9 | import textual.binding
10 | import textual.message
11 | import textual.widget
12 | import textual.widgets
13 | import textual.widgets.tree
14 | import uproot
15 | from rich.style import Style
16 |
17 | from ..tree import UprootEntry
18 |
19 |
20 | @rich.repr.auto
21 | class UprootSelected(textual.message.Message, bubble=True):
22 | def __init__(self, upfile: Any, path: str) -> None:
23 | self.upfile = upfile
24 | self.path = path
25 | super().__init__()
26 |
27 |
28 | class UprootTree(textual.widgets.Tree[UprootEntry]):
29 | """currently just extending DirectoryTree, showing current path"""
30 |
31 | BINDINGS: ClassVar[
32 | list[textual.binding.Binding | tuple[str, str] | tuple[str, str, str]]
33 | ] = [
34 | textual.binding.Binding("h", "cursor_out", "Cursor out", show=False),
35 | textual.binding.Binding("j", "cursor_down", "Cursor Down", show=False),
36 | textual.binding.Binding("k", "cursor_up", "Cursor Up", show=False),
37 | textual.binding.Binding("l", "cursor_in", "Cursor in", show=False),
38 | ]
39 |
40 | def __init__(self, path: str, **args: Any) -> None:
41 | self.upfile = uproot.open(path)
42 | file_path = Path(self.upfile.file_path)
43 | data = UprootEntry("/", self.upfile)
44 | super().__init__(name=str(file_path), data=data, label=file_path.stem, **args)
45 |
46 | def render_label(
47 | self,
48 | node: textual.widgets.tree.TreeNode[UprootEntry],
49 | base_style: Style,
50 | style: Style, # ,
51 | ) -> rich.text.Text:
52 | assert node.data
53 | meta = node.data.meta()
54 | label_icon = rich.text.Text(meta["label_icon"])
55 | label_icon.stylize(base_style)
56 |
57 | label = rich.text.Text.assemble(label_icon, meta["label_text"])
58 | label.stylize(style)
59 | return label
60 |
61 | def on_mount(self) -> None:
62 | self.load_directory(self.root)
63 | self.root.expand()
64 |
65 | def load_directory(self, node: textual.widgets.tree.TreeNode[UprootEntry]) -> None:
66 | assert node.data
67 | if not node.children:
68 | children = node.data.children
69 | for child in children:
70 | node.add(child.path, child)
71 |
72 | def on_tree_node_selected(
73 | self, event: textual.widgets.Tree.NodeSelected[UprootEntry]
74 | ) -> None:
75 | event.stop()
76 | item = event.node.data
77 | assert item
78 | if not item.is_dir:
79 | self.post_message(UprootSelected(self.upfile, item.path))
80 |
81 | def on_tree_node_expanded(
82 | self, event: textual.widgets.Tree.NodeSelected[UprootEntry]
83 | ) -> None:
84 | event.stop()
85 | item = event.node.data
86 | assert item
87 | if item.is_dir:
88 | self.load_directory(event.node)
89 |
90 | def _node_expanded(
91 | self, node: textual.widgets.tree.TreeNode[UprootEntry]
92 | ) -> textual.widgets.Tree.NodeExpanded[UprootEntry]:
93 | try:
94 | return self.NodeExpanded(node)
95 | except TypeError: # textual 0.24-0.26
96 | # pylint: disable-next=too-many-function-args
97 | return self.NodeExpanded(self, node) # type:ignore[call-arg,arg-type]
98 |
99 | def _node_collapsed(
100 | self, node: textual.widgets.tree.TreeNode[UprootEntry]
101 | ) -> textual.widgets.Tree.NodeCollapsed[UprootEntry]:
102 | try:
103 | return self.NodeCollapsed(node)
104 | except TypeError: # textual 0.24-0.26
105 | # pylint: disable-next=too-many-function-args
106 | return self.NodeCollapsed(self, node) # type:ignore[call-arg,arg-type]
107 |
108 | def action_cursor_in(self) -> None:
109 | node = self.cursor_node
110 | if node is None:
111 | return
112 | if node.allow_expand and not node.is_expanded:
113 | node.expand()
114 | self.post_message(self._node_expanded(node))
115 |
116 | def action_cursor_out(self) -> None:
117 | node = self.cursor_node
118 | if node is None:
119 | return
120 | if node.allow_expand and node.is_expanded:
121 | node.collapse()
122 | self.post_message(self._node_collapsed(node))
123 | elif (
124 | node.parent is not None
125 | and node.parent.allow_expand
126 | and node.parent.is_expanded
127 | ):
128 | node.parent.collapse()
129 | self.post_message(self._node_collapsed(node))
130 | self.cursor_line = node.parent.line
131 | self.scroll_to_line(self.cursor_line)
132 |
--------------------------------------------------------------------------------
/src/uproot_browser/tui/right_panel.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import dataclasses
4 | from collections.abc import Iterable
5 | from types import TracebackType
6 | from typing import Any
7 |
8 | import numpy as np
9 | import plotext as plt # plots in text
10 | import rich.panel
11 | import rich.text
12 | import rich.traceback
13 | import textual.widget
14 | import textual.widgets
15 |
16 | try:
17 | from textual.widgets import RichLog
18 | except ImportError:
19 | from textual.widgets import ( # type: ignore[attr-defined,no-redef]
20 | TextLog as RichLog,
21 | )
22 |
23 | import uproot_browser.plot
24 |
25 | LOGO = """\
26 | Scikit-HEP
27 | ┬ ┬┌─┐┬─┐┌─┐┌─┐┌┬┐5 ┌┐ ┬─┐┌─┐┬ ┬┌─┐┌─┐┬─┐
28 | │ │├─┘├┬┘│ ││ │ │───├┴┐├┬┘│ ││││└─┐├┤ ├┬┘
29 | └─┘┴ ┴└─└─┘└─┘ ┴ └─┘┴└─└─┘└┴┘└─┘└─┘┴└─
30 | Powered by Textual & Hist"""
31 |
32 | LOGO_PANEL = rich.text.Text.from_ansi(LOGO, no_wrap=True)
33 |
34 |
35 | placeholder = np.random.rand(1000)
36 |
37 |
38 | def apply_selection(tree: Any, selection: Iterable[str]) -> Iterable[Any]:
39 | """
40 | Apply a colon-separated selection to an uproot tree. Slashes are handled by uproot.
41 | """
42 | for sel in selection:
43 | tree = tree[sel]
44 | yield tree
45 |
46 |
47 | def make_plot(item: Any, theme: str, *size: int) -> Any:
48 | plt.clf()
49 | plt.plotsize(*size)
50 | uproot_browser.plot.plot(item)
51 | plt.theme(theme)
52 | return plt.build()
53 |
54 |
55 | # wrapper for plotext into a textual widget
56 | @dataclasses.dataclass
57 | class Plotext:
58 | upfile: Any
59 | selection: str
60 | theme: str
61 |
62 | def __rich_console__(
63 | self, console: rich.console.Console, options: rich.console.ConsoleOptions
64 | ) -> rich.console.RenderResult:
65 | *_, item = apply_selection(self.upfile, self.selection.split(":"))
66 |
67 | if item is None:
68 | yield rich.text.Text()
69 | return
70 | width = options.max_width or console.width
71 | height = options.height or console.height
72 |
73 | canvas = make_plot(item, self.theme, width, height)
74 | yield rich.text.Text.from_ansi(canvas)
75 |
76 |
77 | class PlotWidget(textual.widget.Widget):
78 | _item: Plotext | None
79 |
80 | @property
81 | def item(self) -> Plotext | None:
82 | return self._item
83 |
84 | @item.setter
85 | def item(self, value: Plotext) -> None:
86 | self._item = value
87 | self.refresh()
88 |
89 | def __init__(self, **kargs: Any):
90 | super().__init__(**kargs)
91 | self._item = None
92 |
93 | def render(self) -> rich.console.RenderableType:
94 | return self.item or ""
95 |
96 |
97 | class EmptyWidget(textual.widget.Widget):
98 | # if the plot is empty
99 |
100 | def render(self) -> rich.console.RenderableType:
101 | return rich.text.Text("Plot is Empty")
102 |
103 |
104 | class LogoWidget(textual.widget.Widget):
105 | def render(self) -> rich.console.RenderableType:
106 | return LOGO_PANEL
107 |
108 |
109 | @dataclasses.dataclass
110 | class Error:
111 | exc: tuple[type[BaseException], BaseException, TracebackType]
112 |
113 | def __rich_console__(
114 | self, console: rich.console.Console, options: rich.console.ConsoleOptions
115 | ) -> rich.console.RenderResult:
116 | width = options.max_width or console.width
117 |
118 | yield rich.traceback.Traceback.from_exception(*self.exc, width=width)
119 |
120 |
121 | class ErrorWidget(RichLog):
122 | _exc: Error | None
123 |
124 | @property
125 | def exc(self) -> Error | None:
126 | return self._exc
127 |
128 | @exc.setter
129 | def exc(self, value: Error) -> None:
130 | self._exc = value
131 | self.clear()
132 | self.write(self._exc)
133 | # self.refresh()
134 |
135 | def __init__(self, **kargs: Any):
136 | super().__init__(**kargs)
137 | self.write("No Exception set!")
138 | self._exc = None
139 |
--------------------------------------------------------------------------------
/tests/test_printouts.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 |
5 | import pytest
6 | import rich.console
7 | from skhep_testdata import data_path
8 |
9 | from uproot_browser.tree import print_tree
10 |
11 | OUT1 = """\
12 | 📁 uproot-Event.root
13 | ┣━━ ❓ TProcessID
14 | ┣━━ 🌴 T (1000)
15 | ┃ ┗━━ 🌿 event Event
16 | ┃ ┣━━ 🌿 TObject (group of fUniqueID:uint32_t, fBits:uint32_t)
17 | ┃ ┃ ┣━━ 🍁 fBits uint32_t
18 | ┃ ┃ ┗━━ 🍁 fUniqueID uint32_t
19 | ┃ ┣━━ 🍁 fClosestDistance unknown[]
20 | ┃ ┣━━ 🍁 fEventName char*
21 | ┃ ┣━━ 🌿 fEvtHdr EventHeader
22 | ┃ ┃ ┣━━ 🍁 fEvtHdr.fDate int32_t
23 | ┃ ┃ ┣━━ 🍁 fEvtHdr.fEvtNum int32_t
24 | ┃ ┃ ┗━━ 🍁 fEvtHdr.fRun int32_t
25 | ┃ ┣━━ 🍁 fFlag uint32_t
26 | ┃ ┣━━ 🍁 fH TH1F
27 | ┃ ┣━━ 🍁 fHighPt TRefArray*
28 | ┃ ┣━━ 🍁 fIsValid bool
29 | ┃ ┣━━ 🍁 fLastTrack TRef
30 | ┃ ┣━━ 🍁 fMatrix[4][4] float[4][4]
31 | ┃ ┣━━ 🍁 fMeasures[10] int32_t[10]
32 | ┃ ┣━━ 🍁 fMuons TRefArray*
33 | ┃ ┣━━ 🍁 fNseg int32_t
34 | ┃ ┣━━ 🍁 fNtrack int32_t
35 | ┃ ┣━━ 🍁 fNvertex uint32_t
36 | ┃ ┣━━ 🍁 fTemperature float
37 | ┃ ┣━━ 🌿 fTracks TClonesArray*
38 | ┃ ┃ ┣━━ 🍃 fTracks.fBits uint32_t[]
39 | ┃ ┃ ┣━━ 🍃 fTracks.fBx Float16_t[]
40 | ┃ ┃ ┣━━ 🍃 fTracks.fBy Float16_t[]
41 | ┃ ┃ ┣━━ 🍃 fTracks.fCharge Double32_t[]
42 | ┃ ┃ ┣━━ 🍃 fTracks.fMass2 Float16_t[]
43 | ┃ ┃ ┣━━ 🍃 fTracks.fMeanCharge float[]
44 | ┃ ┃ ┣━━ 🍃 fTracks.fNpoint int32_t[]
45 | ┃ ┃ ┣━━ 🍃 fTracks.fNsp uint32_t[]
46 | ┃ ┃ ┣━━ 🍁 fTracks.fPointValue unknown[][]
47 | ┃ ┃ ┣━━ 🍃 fTracks.fPx float[]
48 | ┃ ┃ ┣━━ 🍃 fTracks.fPy float[]
49 | ┃ ┃ ┣━━ 🍃 fTracks.fPz float[]
50 | ┃ ┃ ┣━━ 🍃 fTracks.fRandom float[]
51 | ┃ ┃ ┣━━ 🍃 fTracks.fTArray[3] float[][3]
52 | ┃ ┃ ┣━━ 🍁 fTracks.fTriggerBits.fAllBits uint8_t[][]
53 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fBits uint32_t[]
54 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fNbits uint32_t[]
55 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fNbytes uint32_t[]
56 | ┃ ┃ ┣━━ 🍃 fTracks.fTriggerBits.fUniqueID uint32_t[]
57 | ┃ ┃ ┣━━ 🍃 fTracks.fUniqueID uint32_t[]
58 | ┃ ┃ ┣━━ 🍃 fTracks.fValid int16_t[]
59 | ┃ ┃ ┣━━ 🍃 fTracks.fVertex[3] Double32_t[][3]
60 | ┃ ┃ ┣━━ 🍃 fTracks.fXfirst Float16_t[]
61 | ┃ ┃ ┣━━ 🍃 fTracks.fXlast Float16_t[]
62 | ┃ ┃ ┣━━ 🍃 fTracks.fYfirst Float16_t[]
63 | ┃ ┃ ┣━━ 🍃 fTracks.fYlast Float16_t[]
64 | ┃ ┃ ┣━━ 🍃 fTracks.fZfirst Float16_t[]
65 | ┃ ┃ ┗━━ 🍃 fTracks.fZlast Float16_t[]
66 | ┃ ┣━━ 🌿 fTriggerBits TBits
67 | ┃ ┃ ┣━━ 🌿 fTriggerBits.TObject (group of fTriggerBits.fUniqueID:uint32_t, fTriggerBits.fBits:uint32_t)
68 | ┃ ┃ ┃ ┣━━ 🍁 fTriggerBits.fBits uint32_t
69 | ┃ ┃ ┃ ┗━━ 🍁 fTriggerBits.fUniqueID uint32_t
70 | ┃ ┃ ┣━━ 🍃 fTriggerBits.fAllBits uint8_t[]
71 | ┃ ┃ ┣━━ 🍁 fTriggerBits.fNbits uint32_t
72 | ┃ ┃ ┗━━ 🍁 fTriggerBits.fNbytes uint32_t
73 | ┃ ┣━━ 🍁 fType[20] int8_t[20]
74 | ┃ ┗━━ 🍁 fWebHistogram TRef
75 | ┣━━ 📊 hstat TH1F (100)
76 | ┗━━ 📊 htime TH1F (10)
77 | """
78 |
79 |
80 | @pytest.mark.xfail(
81 | sys.platform.startswith("win"),
82 | reason="Unicode is different on Windows, for some reason?",
83 | )
84 | def test_tree(capsys):
85 | filename = data_path("uproot-Event.root")
86 | console = rich.console.Console(width=120)
87 |
88 | print_tree(filename, console=console)
89 | out, err = capsys.readouterr()
90 |
91 | assert not err
92 | assert out == OUT1
93 |
--------------------------------------------------------------------------------
/tests/test_tui.py:
--------------------------------------------------------------------------------
1 | import skhep_testdata
2 |
3 | from uproot_browser.tui.browser import Browser
4 |
5 |
6 | async def test_browse_logo() -> None:
7 | async with Browser(
8 | skhep_testdata.data_path("uproot-Event.root")
9 | ).run_test() as pilot:
10 | assert pilot.app.query_one("#main-view").current == "logo"
11 |
12 |
13 | async def test_browse_plot() -> None:
14 | async with Browser(
15 | skhep_testdata.data_path("uproot-Event.root")
16 | ).run_test() as pilot:
17 | await pilot.press("down", "down", "down", "enter")
18 | assert pilot.app.query_one("#main-view").current == "plot"
19 |
20 |
21 | async def test_browse_empty() -> None:
22 | async with Browser(
23 | skhep_testdata.data_path("uproot-empty.root")
24 | ).run_test() as pilot:
25 | await pilot.press("down", "space", "down", "enter")
26 | assert pilot.app.query_one("#main-view").current == "empty"
27 |
28 |
29 | async def test_browse_empty_vim() -> None:
30 | async with Browser(
31 | skhep_testdata.data_path("uproot-empty.root")
32 | ).run_test() as pilot:
33 | await pilot.press("j", "l", "j", "enter")
34 | assert pilot.app.query_one("#main-view").current == "empty"
35 |
36 |
37 | async def test_help_focus() -> None:
38 | async with Browser(
39 | skhep_testdata.data_path("uproot-empty.root")
40 | ).run_test() as pilot:
41 | await pilot.press("?")
42 | focus_chain = [widget.id for widget in pilot.app.screen.focus_chain]
43 | assert len(focus_chain) == 3
44 | assert focus_chain[-1] == "help-done"
45 |
--------------------------------------------------------------------------------