Skip to content

Commit 2953bfd

Browse files
authored
Housekeeping (DistrictDataLabs#1187)
This PR fixes DistrictDataLabs#1113 by handling some housekeeping issues. We've changed our master branch to the main branch and created a release issue template (though I don't think it is used automatically by GitHub). We've also checked the new pip resolver and all of our dependencies are compatible. This PR also updates our CI to test Python 3.8 and 3.9 to ensure we're testing on the latest version of Python.
1 parent 9b26ee2 commit 2953bfd

File tree

16 files changed

+179
-79
lines changed

16 files changed

+179
-79
lines changed

.gitattributes

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Set the default behavior, in case contributors don't have core.autocrlf set.
2+
* text=auto
3+
4+
# Explicitly declare text files
5+
*.py text
6+
*.rst text
7+
*.md text
8+
*.json text
9+
*.ipynb text
10+
*.cfg text
11+
12+
# Baseline images are binary and should not be modified
13+
*.png binary
14+
*.jpg binary
15+
*.pdf binary
16+
17+
# Compressed files are binary and should not be modified
18+
*.gz binary
19+
*.npz binary
20+
*.zip binary

.github/RELEASE_TEMPLATE.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
**Deployed:** DayOfWeek, Month D, YYYY
2+
**Current Contributors:** [insert GitHub @username comma list]
3+
4+
[Release description (1-2 paras)]
5+
6+
**Major Changes:**
7+
8+
- [Change 1]
9+
- [Change 2]
10+
11+
Minor Changes:
12+
13+
- [Change 1]
14+
- [Change 2]

.github/workflows/ci.yml

+79-55
Original file line numberDiff line numberDiff line change
@@ -3,104 +3,128 @@
33

44
name: Yellowbrick CI
55

6-
on:
7-
# Trigger on push to the primary branches only
6+
on:
7+
# Trigger on push to the primary branches and all versioned tags
88
push:
99
branches:
1010
- develop
1111
- main
12-
- master
12+
tags:
13+
- 'v*'
1314
# Trigger on pull request always (note the trailing colon)
1415
pull_request:
1516

1617
jobs:
18+
# Test Vanilla Python (using PyPI for dependencies) as a matrix.
19+
test_pypi:
20+
runs-on: ${{ matrix.os }}
21+
name: PyPi (${{ matrix.python-version }}, ${{ matrix.os }})
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
python-version: [3.8, 3.9]
26+
os: [ubuntu-latest, macos-latest, windows-latest]
27+
28+
steps:
29+
- name: Checkout Code
30+
uses: actions/checkout@v2
31+
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v2
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
37+
- name: Install Dependencies
38+
run: |
39+
python -m pip install --upgrade pip
40+
pip install -r tests/requirements.txt
41+
python -m nltk.downloader popular
42+
43+
- name: Run Unit Tests
44+
run: |
45+
make test
46+
47+
- name: Upload Coverage
48+
uses: codecov/codecov-action@v1
49+
with:
50+
env_vars: OS,PYTHON
51+
name: codecov-umbrella
52+
fail_ci_if_error: false
53+
54+
# Test Anaconda Python (using conda-forge for dependencies)
1755
test_conda:
18-
name: Conda (${{ matrix.python-version }}, ${{ matrix.os }})
1956
runs-on: ${{ matrix.os }}
57+
name: Conda (${{ matrix.python-version }}, ${{ matrix.os }})
2058
strategy:
2159
fail-fast: false
2260
matrix:
2361
os: ["ubuntu-latest"]
24-
python-version: ["3.7", "3.8"]
62+
python-version: ["3.8", "3.9"]
63+
2564
steps:
26-
- uses: actions/checkout@v2
27-
- uses: conda-incubator/setup-miniconda@v2
65+
- name: Checkout Code
66+
uses: actions/checkout@v2
67+
68+
- name: Setup Conda ${{ matrix.python-version }}
69+
uses: conda-incubator/setup-miniconda@v2
2870
with:
2971
auto-update-conda: true
3072
python-version: ${{ matrix.python-version }}
3173
channels: conda-forge,spyder-ide
3274
channel-priority: flexible
3375

34-
- name: Conda info
76+
- name: Conda Info
3577
shell: bash -l {0}
3678
run: conda info
37-
- name: Add conda to system path
79+
80+
- name: Add conda to System $PATH
3881
shell: bash -l {0}
3982
run: |
4083
# $CONDA is an environment variable pointing to the root of the miniconda directory
4184
echo $CONDA/bin >> $GITHUB_PATH
42-
- name: Install dependencies
85+
86+
- name: Install Dependencies
4387
shell: bash -l {0}
4488
run: |
4589
conda config --set always_yes yes --set changeps1 no
4690
conda update -n base conda --yes
4791
conda env create -f tests/requirements.txt -n yellowbrick python=${{ matrix.python-version }}
48-
- name: Test with pytest
92+
conda activate yellowbrick
93+
python -m nltk.downloader popular
94+
95+
- name: Run Unit Tests
4996
shell: bash -l {0}
5097
run: |
5198
conda activate yellowbrick
52-
python -m nltk.downloader popular
5399
make test
54100
55-
- name: Upload coverage to Codecov
101+
- name: Upload Coverage
56102
uses: codecov/codecov-action@v1
57103
with:
58104
env_vars: OS,PYTHON
59105
name: codecov-umbrella
60106
fail_ci_if_error: false
61107

62-
63-
test_pypi:
64-
name: PyPi (${{ matrix.python-version }}, ${{ matrix.os }})
65-
strategy:
66-
fail-fast: false
67-
matrix:
68-
python-version: [3.7, 3.8]
69-
os: [ubuntu-latest, macos-latest, windows-latest]
70-
71-
runs-on: ${{ matrix.os }}
72-
108+
# Test building docs using Sphinx since this also runs a lot of Yellowbrick code
109+
test_docs:
110+
runs-on: ubuntu-latest
111+
name: Build Docs
73112
steps:
74-
- uses: actions/checkout@v2
75-
- name: Set up Python ${{ matrix.python-version }}
76-
uses: actions/setup-python@v2
77-
with:
78-
python-version: ${{ matrix.python-version }}
79-
- name: Install dependencies
80-
run: |
81-
python -m pip install --upgrade pip
82-
pip install -r tests/requirements.txt
83-
- name: Test with pytest
84-
run: |
85-
python -m nltk.downloader popular
86-
make test
87-
- name: Upload coverage to Codecov
88-
uses: codecov/codecov-action@v1
89-
with:
90-
env_vars: OS,PYTHON
91-
name: codecov-umbrella
92-
fail_ci_if_error: false
113+
- name: Checkout Code
114+
uses: actions/checkout@v2
93115

116+
- name: Set up Python
117+
uses: actions/setup-python@v2
118+
with:
119+
python-version: 3.9
120+
121+
- name: Install Dependencies
122+
run: |
123+
python -m pip install --upgrade pip
124+
pip install -r requirements.txt
125+
pip install -r docs/requirements.txt
94126
95-
# slackNotification:
96-
# name: Slack Notification
97-
# needs: [test_pypi, test_conda]
98-
# runs-on: ubuntu-latest
99-
# steps:
100-
# - uses: actions/checkout@v2
101-
# - name: Slack Notification
102-
# uses: rtCamp/action-slack-notify@v2
103-
# env:
104-
# SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
105-
# SLACK_CHANNEL: yb-ci
106-
# SLACK_USERNAME: "Github Actions"
127+
- name: Run Sphinx
128+
uses: ammaraskar/sphinx-action@master
129+
with:
130+
docs-folder: "docs/"

docs/api/model_selection/cross_validation.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ In the following example, we show how to visualize cross-validated scores for a
4141
X, y = load_occupancy()
4242

4343
# Create a cross-validation strategy
44-
cv = StratifiedKFold(n_splits=12, random_state=42)
44+
cv = StratifiedKFold(n_splits=12, shuffle=True, random_state=42)
4545

4646
# Instantiate the classification model and visualizer
4747
model = MultinomialNB()
@@ -72,7 +72,7 @@ In this next example we show how to visualize cross-validated scores for a regre
7272
X, y = load_energy()
7373

7474
# Instantiate the regression model and visualizer
75-
cv = KFold(n_splits=12, random_state=42)
75+
cv = KFold(n_splits=12, shuffle=True, random_state=42)
7676

7777
model = Ridge()
7878
visualizer = CVScores(model, cv=cv, scoring='r2')
@@ -102,7 +102,7 @@ Quick Method
102102
X, y = load_energy()
103103

104104
# Instantiate the regression model and visualizer
105-
cv = KFold(n_splits=12, random_state=42)
105+
cv = KFold(n_splits=12, shuffle=True, random_state=42)
106106

107107
model = Ridge()
108108
visualizer = cv_scores(model, X, y, cv=cv, scoring='r2')

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@
232232

233233

234234
def setup(app):
235-
app.add_stylesheet("theme_overrides.css")
235+
app.add_css_file("theme_overrides.css")
236236

237237

238238
# Add any extra paths that contain custom files (such as robots.txt or

docs/contributing/developing_visualizers.rst

+26-4
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,14 @@ Visual tests are notoriously difficult to create --- how do you test a visualiza
139139
try:
140140
visualizer = MyVisualizer()
141141
assert visualizer.fit(X, y) is visualizer, "fit should return self"
142-
visualizer.show()
142+
visualizer.finalize()
143143
except Exception as e:
144144
pytest.fail("my visualizer didn't work: {}".format(e))
145145
146146
This simple test case is an excellent start to a larger test package and we recommend starting with this test as you develop your visualizer. Once you've completed the development and prototyping you can start to include :ref:`test fixtures <fixtures>` and test various normal use cases and edge cases with unit tests, then build :ref:`image similarity tests <assert_images_similar>` to more thoroughly define the integration tests.
147147

148+
.. note:: In tests you should not call ``visualizer.show()`` because this will call ``plt.show()`` and trigger a matplotlib warning that the visualization cannot be displayed using the test backend ``Agg``. Calling ``visualizer.finalize()`` instead should produce the full image and make the tests faster and more readable.
149+
148150

149151
Running the Test Suite
150152
~~~~~~~~~~~~~~~~~~~~~~
@@ -157,9 +159,15 @@ The required dependencies for the test suite include testing utilities and libra
157159

158160
Tests can be run as follows from the project root::
159161

160-
$ make test
162+
$ pytest
163+
164+
The ``pytest`` function is configured via ``setup.cfg`` with the correct arguments and runner, and therefore must be run from the project root. You can also use ``make test`` but this simply runs the ``pytest`` command.
165+
166+
The tests do take a while to run, so during normal development it is recommended that you only run the tests for the test file you're writing::
167+
168+
$ pytest tests/test_package/test_module.py
161169

162-
The Makefile uses the pytest runner and testing suite as well as the coverage library.
170+
This will greatly simplify development and allow you to focus on the changes that you're making!
163171

164172
.. _assert_images_similar:
165173

@@ -181,7 +189,7 @@ For example, create your test function located in ``tests/test_regressor/test_my
181189
def test_my_visualizer_output(self):
182190
visualizer = MyVisualizer()
183191
visualizer.fit(X)
184-
visualizer.show()
192+
visualizer.finalize()
185193
self.assert_images_similar(visualizer)
186194
187195
The first time this test is run, there will be no baseline image to compare against, so the test will fail. Copy the output images (in this case ``tests/actual_images/test_regressor/test_myvisualizer/test_my_visualizer_output.png``) to the correct subdirectory of baseline_images tree in the source directory (in this case ``tests/baseline_images/test_regressor/test_myvisualizer/test_my_visualizer_output.png``). Put this new file under source code revision control (with git add). When rerunning the tests, they should now pass.
@@ -196,6 +204,20 @@ This will move all related test images from ``actual_images`` to ``baseline_imag
196204

197205
This is useful particularly if you're stuck trying to get an image comparison to work. For more information on the images helper script, use ``python -m tests.images --help``.
198206

207+
Finally, to reiterate a note from above; make sure that you do not call your visualizer's ``show()`` method, instead using ``finalize()`` directly. If you call ``show()``, it will in turn call ``plt.show()`` which will issue a warning due to the fact that the tests use the ``Agg`` backend. When testing quick methods you should pass the ``show=False`` argument to the quick method as follows:
208+
209+
.. code:: python
210+
211+
from tests.base import VisualTestCase
212+
213+
class MyVisualizerTests(VisualTestCase):
214+
215+
def test_my_visualizer_quickmethod(self):
216+
visualizer = my_visualizer_quickmethod(X, show=False)
217+
self.assert_images_similar(visualizer)
218+
219+
Generally speaking, testing quick methods is identical to testing Visualizers except for the method of interaction because the quick method will return the visualizer or the axes object.
220+
199221
.. _fixtures:
200222

201223
Test Fixtures

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"Programming Language :: Python :: 3.6",
6565
"Programming Language :: Python :: 3.7",
6666
"Programming Language :: Python :: 3.8",
67+
"Programming Language :: Python :: 3.9",
6768
"Topic :: Software Development",
6869
"Topic :: Software Development :: Libraries :: Python Modules",
6970
"Topic :: Scientific/Engineering :: Visualization",

tests/base.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,13 @@ class ImageComparison(object):
164164
165165
visualizer : Yellowbrick Visualizer instance, optional
166166
An instantiated Yellowbrick visualizer that wraps a matplotlib Axes
167-
and has been drawn on via the Yellowbrick API.
167+
and has been drawn on via the Yellowbrick API. If a visualizer is specified,
168+
generally speaking it should have a call to ``finalize()`` performed, but not a
169+
call to ``show()``.
168170
169171
ax : matplotlib Axes, optional
170-
A direct reference to a matplotlib Axes that has been drawn on.
172+
A direct reference to a matplotlib Axes that has been drawn on. If using the
173+
Axes object, you should not call ``plt.show()``.
171174
172175
tol : float, default: 0.01
173176
The tolerance as a color value difference, where 255 is the maximal

tests/test_classifier/test_prcurve.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,10 @@ def test_quick_method_with_test_set(self):
435435
)
436436

437437
viz = precision_recall_curve(
438-
RandomForestClassifier(random_state=72), X_train, y_train, X_test, y_test
438+
RandomForestClassifier(random_state=72),
439+
X_train, y_train,
440+
X_test, y_test,
441+
show=False
439442
)
440443
self.assert_images_similar(viz)
441444

@@ -453,11 +456,13 @@ def test_missing_test_data_in_quick_method(self):
453456

454457
with pytest.raises(YellowbrickValueError, match=emsg):
455458
precision_recall_curve(
456-
RandomForestClassifier(), X_train, y_train, y_test=y_test
459+
RandomForestClassifier(), X_train, y_train, y_test=y_test, show=False
457460
)
458461

459462
with pytest.raises(YellowbrickValueError, match=emsg):
460-
precision_recall_curve(RandomForestClassifier(), X_train, y_train, X_test)
463+
precision_recall_curve(
464+
RandomForestClassifier(), X_train, y_train, X_test, show=False
465+
)
461466

462467
def test_per_class_and_micro(self):
463468
"""
@@ -469,5 +474,5 @@ def test_per_class_and_micro(self):
469474
)
470475
with pytest.warns(YellowbrickWarning, match=msg):
471476
PrecisionRecallCurve(
472-
RidgeClassifier(random_state=13), micro=True, per_class=True
477+
RidgeClassifier(random_state=13), micro=True, per_class=True, show=False
473478
)

0 commit comments

Comments
 (0)