")
def clear_table(self) -> None:
"""Clear the table that shows the available dimensions."""
self.dimension_table.clear()
self.dimension_table.setColumnCount(5)
self.dimension_table.setHorizontalHeaderLabels(
- ['Type', 'First', 'Current', 'Last', 'Units'])
+ ["Type", "First", "Current", "Last", "Units"]
+ )
self.dimension_table.setRowCount(0)
def addLayout(self, layout: QtWidgets.QLayout) -> QtWidgets.QWidget:
@@ -643,11 +671,11 @@ def animate_backward(self) -> None:
"""Start the current animation in backward direction, or stop it."""
if self._animating:
self.stop_animation()
- self.btn_animate_backward.setText('◀◀')
+ self.btn_animate_backward.setText("◀◀")
self.enable_navigation()
else:
self._animate_forward = False
- self.btn_animate_backward.setText('■')
+ self.btn_animate_backward.setText("■")
self.disable_navigation(self.btn_animate_backward)
self.start_animation()
@@ -655,22 +683,20 @@ def animate_forward(self, nframes=None):
"""Start the current animation in forward direction, or stop it."""
if self._animating:
self.stop_animation()
- self.btn_animate_forward.setText('▶▶')
+ self.btn_animate_forward.setText("▶▶")
self.enable_navigation()
else:
self._animate_forward = True
- self.btn_animate_forward.setText('■')
+ self.btn_animate_forward.setText("■")
self.disable_navigation(self.btn_animate_forward)
self.start_animation(nframes)
def setup_plot_tabs(self) -> None:
"""Setup the tabs of the various plot methods."""
- self.plot_tabs.addTab(MapPlotWidget(self.get_sp, self.ds),
- 'mapplot')
- self.plot_tabs.addTab(Plot2DWidget(self.get_sp, self.ds),
- 'plot2d')
+ self.plot_tabs.addTab(MapPlotWidget(self.get_sp, self.ds), "mapplot")
+ self.plot_tabs.addTab(Plot2DWidget(self.get_sp, self.ds), "plot2d")
lineplot_widget = LinePlotWidget(self.get_sp, self.ds)
- self.plot_tabs.addTab(lineplot_widget, 'lineplot')
+ self.plot_tabs.addTab(lineplot_widget, "lineplot")
for w in map(self.plot_tabs.widget, range(self.plot_tabs.count())):
w.replot.connect(self.replot)
@@ -713,8 +739,8 @@ def reset(self, plotmethod: str) -> None:
self.refresh()
def disable_navigation(
- self, but: Optional[QtWidgets.QPushButton] = None
- ) -> None:
+ self, but: Optional[QtWidgets.QPushButton] = None
+ ) -> None:
"""Disable the navigation buttons.
This function disables all navigation buttons but the one you specify.
@@ -724,16 +750,18 @@ def disable_navigation(
but: PyQt5.QtWidgets.QPushButton
If not None, this button is not disabled.
"""
- for item in map(self.navigation_box.itemAt,
- range(self.navigation_box.count())):
+ for item in map(
+ self.navigation_box.itemAt, range(self.navigation_box.count())
+ ):
w = item.widget()
if w is not but and w is not self.sl_interval:
w.setEnabled(False)
def enable_navigation(self) -> None:
"""Enable all navigation buttons again."""
- for item in map(self.navigation_box.itemAt,
- range(self.navigation_box.count())):
+ for item in map(
+ self.navigation_box.itemAt, range(self.navigation_box.count())
+ ):
w = item.widget()
w.setEnabled(True)
@@ -768,9 +796,13 @@ def start_animation(self, nframes: Optional[int] = None):
if self.sp is not None:
if self.animation is None or self.animation.event_source is None:
self.animation = FuncAnimation(
- self.fig, self.update_dims, frames=self.animation_frames(),
- init_func=self.sp.draw, interval=self.sl_interval.value(),
- repeat=False)
+ self.fig,
+ self.update_dims,
+ frames=self.animation_frames(),
+ init_func=self.sp.draw,
+ interval=self.sl_interval.value(),
+ repeat=False,
+ )
# HACK: Make sure that the animation starts although the figure
# is already shown
self.animation._draw_frame(next(self.animation_frames()))
@@ -779,7 +811,7 @@ def start_animation(self, nframes: Optional[int] = None):
def reset_timer_interval(self, value: int) -> None:
"""Change the interval of the timer."""
- self.lbl_interval.setText('%i ms' % value)
+ self.lbl_interval.setText("%i ms" % value)
if self.animation is None or self.animation.event_source is None:
pass
else:
@@ -791,8 +823,10 @@ def reset_timer_interval(self, value: int) -> None:
def stop_animation(self) -> None:
"""Stop the current animation."""
self._animating = False
- if (self.animation is not None and
- self.animation.event_source is not None):
+ if (
+ self.animation is not None
+ and self.animation.event_source is not None
+ ):
self.animation.event_source.stop()
self.plot_tabs.setEnabled(True)
self.enable_variables()
@@ -800,8 +834,11 @@ def stop_animation(self) -> None:
def animation_frames(self) -> Iterator[Dict[str, int]]:
"""Get the animation frames for the :attr:`combo_dims` dimension."""
- while self._animating and self._animation_frames is None or \
- self._animation_frames:
+ while (
+ self._animating
+ and self._animation_frames is None
+ or self._animation_frames
+ ):
if self._animation_frames is not None and not self._init_step:
self._animation_frames -= 1
dim = self.combo_dims.currentText()
@@ -824,9 +861,11 @@ def update_dims(self, dims: Dict[str, Any]):
def _load_preset(self) -> None:
"""Open a file dialog and load the selected preset."""
fname, ok = QtWidgets.QFileDialog.getOpenFileName(
- self, 'Load preset', osp.join(get_configdir(), 'presets'),
- 'YAML files (*.yml *.yaml);;'
- 'All files (*)')
+ self,
+ "Load preset",
+ osp.join(get_configdir(), "presets"),
+ "YAML files (*.yml *.yaml);;" "All files (*)",
+ )
if ok:
self.load_preset(fname)
@@ -852,17 +891,21 @@ def preset(self) -> Dict[str, Any]:
if self._preset is None:
return {}
import psyplot.project as psy
+
preset = self._preset
try:
preset = psy.Project._load_preset(preset)
except yaml.constructor.ConstructorError:
answer = QtWidgets.QMessageBox.question(
- self, "Can I trust this?",
+ self,
+ "Can I trust this?",
f"Failed to load the preset at {preset} in safe mode. Can we "
- "trust this preset and load it in unsafe mode?")
+ "trust this preset and load it in unsafe mode?",
+ )
if answer == QtWidgets.QMessageBox.Yes:
- psyd.rcParams['presets.trusted'].append(
- psy.Project._resolve_preset_path(preset))
+ psyd.rcParams["presets.trusted"].append(
+ psy.Project._resolve_preset_path(preset)
+ )
preset = psy.Project._load_preset(preset)
else:
preset = {}
@@ -872,7 +915,6 @@ def preset(self) -> Dict[str, Any]:
def preset(self, value: Optional[Union[str, Dict[str, Any]]]):
self._preset = value
-
def unset_preset(self) -> None:
"""Unset the current preset and do not use it anymore."""
self.preset = None # type: ignore
@@ -881,12 +923,13 @@ def unset_preset(self) -> None:
def maybe_show_preset(self) -> None:
"""Show the name of the current preset if one is selected."""
if self._preset is not None and isinstance(self._preset, str):
- self.lbl_preset.setText('' +
- osp.basename(osp.splitext(self._preset)[0]) + '')
+ self.lbl_preset.setText(
+ "" + osp.basename(osp.splitext(self._preset)[0]) + ""
+ )
self.lbl_preset.setVisible(True)
self.btn_unset_preset.setVisible(True)
elif self._preset is not None:
- self.lbl_preset.setText('custom')
+ self.lbl_preset.setText("custom")
self.lbl_preset.setVisible(True)
self.btn_unset_preset.setVisible(True)
else:
@@ -916,9 +959,11 @@ def _save_preset(self, save_func: Callable[[str], Any]) -> None:
path as an argument
"""
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
- self, 'Save preset', osp.join(get_configdir(), 'presets'),
- 'YAML files (*.yml *.yaml);;'
- 'All files (*)')
+ self,
+ "Save preset",
+ osp.join(get_configdir(), "presets"),
+ "YAML files (*.yml *.yaml);;" "All files (*)",
+ )
if not ok:
return None
save_func(fname)
@@ -929,18 +974,25 @@ def setup_preset_menu(self) -> QtWidgets.QMenu:
self._save_preset_actions = []
self._load_preset_action = menu.addAction(
- "Load preset", self._load_preset)
+ "Load preset", self._load_preset
+ )
self._unset_preset_action = menu.addAction(
- "Unset preset", self.unset_preset)
+ "Unset preset", self.unset_preset
+ )
menu.addSeparator()
self._save_preset_actions.append(
- menu.addAction('Save format of current plot as preset',
- self.save_current_preset))
+ menu.addAction(
+ "Save format of current plot as preset",
+ self.save_current_preset,
+ )
+ )
self._save_preset_actions.append(
- menu.addAction('Save format of all plots as preset',
- self.save_full_preset))
+ menu.addAction(
+ "Save format of all plots as preset", self.save_full_preset
+ )
+ )
for action in self._save_preset_actions:
action.setEnabled(False)
@@ -950,13 +1002,14 @@ def setup_preset_menu(self) -> QtWidgets.QMenu:
def setup_export_menu(self) -> QtWidgets.QMenu:
"""Set up the menu to export the current plot."""
self.export_menu = menu = QtWidgets.QMenu()
- menu.addAction('image (PDF, PNG, etc.)', self.export_image)
- menu.addAction('all images (PDF, PNG, etc.)', self.export_all_images)
- menu.addAction('animation (GIF, MP4, etc.', self.export_animation)
- menu.addAction('psyplot project (.pkl file)', self.export_project)
- menu.addAction('psyplot project with data',
- self.export_project_with_data)
- py_action = menu.addAction('python script (.py)', self.export_python)
+ menu.addAction("image (PDF, PNG, etc.)", self.export_image)
+ menu.addAction("all images (PDF, PNG, etc.)", self.export_all_images)
+ menu.addAction("animation (GIF, MP4, etc.", self.export_animation)
+ menu.addAction("psyplot project (.pkl file)", self.export_project)
+ menu.addAction(
+ "psyplot project with data", self.export_project_with_data
+ )
+ py_action = menu.addAction("python script (.py)", self.export_python)
py_action.setEnabled(False) # psyplot does not yet export to python
return menu
@@ -964,36 +1017,46 @@ def export_image(self) -> None:
"""Ask for a filename and export the current plot to a file."""
if self.sp is not None:
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
- self, "Export image", os.getcwd(),
- "Images (*.png *.pdf *.jpg *.svg)")
+ self,
+ "Export image",
+ os.getcwd(),
+ "Images (*.png *.pdf *.jpg *.svg)",
+ )
if ok:
- self.sp.export(fname, **rcParams['savefig_kws'])
+ self.sp.export(fname, **rcParams["savefig_kws"])
def export_all_images(self) -> None:
"""Ask for a filename and export all plots to one (or more) files."""
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
- self, "Export image", os.getcwd(),
- "Images (*.png *.pdf *.jpg *.svg)")
+ self,
+ "Export image",
+ os.getcwd(),
+ "Images (*.png *.pdf *.jpg *.svg)",
+ )
if ok and self._sp:
# test filenames
- if not osp.splitext(fname)[-1].lower() == '.pdf':
+ if not osp.splitext(fname)[-1].lower() == ".pdf":
fnames = [
sp.format_string(fname, False, i)
- for i, sp in enumerate(self._sp.figs.values())]
+ for i, sp in enumerate(self._sp.figs.values())
+ ]
if len(fnames) != len(set(fnames)):
answer = QtWidgets.QMessageBox.question(
- self, "Too many figures",
+ self,
+ "Too many figures",
TOO_MANY_FIGURES_WARNING.format(
- len(set(fnames)), ', '.join(set(fnames))))
+ len(set(fnames)), ", ".join(set(fnames))
+ ),
+ )
if answer == QtWidgets.QMessageBox.No:
return
- self._sp.export(fname, **rcParams['savefig_kws'])
+ self._sp.export(fname, **rcParams["savefig_kws"])
def export_animation(self) -> None:
"""Ask for a filename and export the animation."""
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
- self, "Export animation", os.getcwd(),
- "Movie (*.mp4 *.mov *.gif)")
+ self, "Export animation", os.getcwd(), "Movie (*.mp4 *.mov *.gif)"
+ )
if ok:
dim = self.combo_dims.currentText()
nframes: int = self.ds.dims[dim] # type: ignore
@@ -1002,8 +1065,10 @@ def export_animation(self) -> None:
self.animate_forward(nframes)
if self.animation is not None:
self.animation.save(
- fname, **rcParams['animations.export_kws'],
- fps=round(1000. / self.sl_interval.value()))
+ fname,
+ **rcParams["animations.export_kws"],
+ fps=round(1000.0 / self.sl_interval.value()),
+ )
self.animate_forward()
self.animation = None
@@ -1011,8 +1076,8 @@ def export_project(self) -> None:
"""Ask for a filename and export the psyplot project as .pkl file."""
if self.sp is not None:
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
- self, "Export project", os.getcwd(),
- "Psyplot projects (*.pkl)")
+ self, "Export project", os.getcwd(), "Psyplot projects (*.pkl)"
+ )
if ok:
self.sp.save_project(fname)
@@ -1023,8 +1088,8 @@ def export_project_with_data(self) -> None:
"""
if self.sp is not None:
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
- self, "Export project", os.getcwd(),
- "Psyplot projects (*.pkl)")
+ self, "Export project", os.getcwd(), "Psyplot projects (*.pkl)"
+ )
if ok:
self.sp.save_project(fname, ds_description={"ds"})
@@ -1105,19 +1170,23 @@ def plot(self) -> PlotterInterface:
return getattr(self.ds.psy.plot, self.plotmethod)
else:
raise ValueError(
- "No dataset has yet been selected, so no plot method!")
+ "No dataset has yet been selected, so no plot method!"
+ )
@property
def plot_options(self) -> Dict[str, Any]:
"""Get further keyword arguments for the :attr:`plot` function."""
if self.ds is not None:
ret: Dict[str, Any] = self.plotmethod_widget.get_fmts( # type: ignore
- self.ds.psy[self.variable], True)
+ self.ds.psy[self.variable], True
+ )
preset = self.preset
if preset:
import psyplot.project as psy
+
preset = psy.Project.extract_fmts_from_preset(
- preset, self.plotmethod)
+ preset, self.plotmethod
+ )
ret.update(dict(preset))
return ret
return {}
@@ -1129,8 +1198,14 @@ def plotmethod(self) -> str:
@plotmethod.setter
def plotmethod(self, label: str):
- i = next((i for i in range(self.plot_tabs.count())
- if self.plot_tabs.tabText(i) == label), None)
+ i = next(
+ (
+ i
+ for i in range(self.plot_tabs.count())
+ if self.plot_tabs.tabText(i) == label
+ ),
+ None,
+ )
if i is not None:
self.plot_tabs.setCurrentIndex(i)
@@ -1143,15 +1218,25 @@ def plotmethods(self) -> List[str]:
def plotmethod_widget(self) -> PlotMethodWidget:
"""Get widget of the current plotmethod."""
label = self.plotmethod
- i = next((i for i in range(self.plot_tabs.count())
- if self.plot_tabs.tabText(i) == label), None)
+ i = next(
+ (
+ i
+ for i in range(self.plot_tabs.count())
+ if self.plot_tabs.tabText(i) == label
+ ),
+ None,
+ )
return self.plot_tabs.widget(i)
@property
def plotmethod_widgets(self) -> Dict[str, PlotMethodWidget]:
"""Get a list of available plotmethod widgets."""
- return dict(zip(self.plotmethods, map(self.plot_tabs.widget,
- range(self.plot_tabs.count()))))
+ return dict(
+ zip(
+ self.plotmethods,
+ map(self.plot_tabs.widget, range(self.plot_tabs.count())),
+ )
+ )
_sp = None
@@ -1162,8 +1247,7 @@ def get_sp(self) -> Optional[Project]:
return self.filter_sp(sp)
def filter_sp(self, sp: Project, ds_only: bool = False) -> Project:
- """Filter the psyplot project to only include the arrays of :attr:`ds`
- """
+ """Filter the psyplot project to only include the arrays of :attr:`ds`"""
if self._new_plot:
return None
if self.ds is None:
@@ -1171,8 +1255,8 @@ def filter_sp(self, sp: Project, ds_only: bool = False) -> Project:
num = self.ds.psy.num
ret = sp[:0]
for i in range(len(sp)):
- if list(sp[i:i+1].datasets) == [num]:
- ret += sp[i:i+1]
+ if list(sp[i : i + 1].datasets) == [num]:
+ ret += sp[i : i + 1]
if ds_only:
return ret
arr_name = self.arr_name
@@ -1192,8 +1276,11 @@ def new_plot(self) -> None:
"""
if self.ds is not None:
name, ok = QtWidgets.QInputDialog.getItem(
- self, 'New plot', 'Select a variable',
- self.plotmethod_widget.valid_variables(self.ds))
+ self,
+ "New plot",
+ "Select a variable",
+ self.plotmethod_widget.valid_variables(self.ds),
+ )
if not ok:
return
with self.silence_variable_buttons():
@@ -1218,8 +1305,9 @@ def sp(self) -> Optional[Project]:
@sp.setter
def sp(self, sp: Optional[Project]):
- if sp is None and (not self._sp or not getattr(
- self._sp, self.plotmethod)):
+ if sp is None and (
+ not self._sp or not getattr(self._sp, self.plotmethod)
+ ):
pass
else:
# first remove the current arrays
@@ -1278,11 +1366,14 @@ def make_plot(self) -> None:
if plotmethod not in plotmethods:
if not plotmethods:
QtWidgets.QMessageBox.critical(
- self, "Visualization impossible",
- f"Found no plotmethod for variable {self.variable}")
+ self,
+ "Visualization impossible",
+ f"Found no plotmethod for variable {self.variable}",
+ )
return
plotmethod, ok = QtWidgets.QInputDialog.getItem(
- self, "Choose a plot method", "Plot method:", plotmethods)
+ self, "Choose a plot method", "Plot method:", plotmethods
+ )
if not ok:
return
self.plotmethod = plotmethod
@@ -1311,7 +1402,8 @@ def make_plot(self) -> None:
self.sp = sp = self.plot(name=self.variable, **self.plot_options)
self._preset = None
cid = sp.plotters[0].ax.figure.canvas.mpl_connect(
- 'button_press_event', self.display_line)
+ "button_press_event", self.display_line
+ )
self.cids[self.plotmethod] = cid
self.show_fig(sp)
descr = sp[0].psy._short_info()
@@ -1336,31 +1428,37 @@ def display_line(self, event: MouseEvent) -> None:
return
else:
sl = None
- for widget in map(self.plot_tabs.widget,
- range(self.plot_tabs.count())):
+ for widget in map(
+ self.plot_tabs.widget, range(self.plot_tabs.count())
+ ):
if widget.sp and event.inaxes == widget.plotter.ax:
sl = widget.get_slice(event.xdata, event.ydata)
break
variable = widget.data.name
raw_data = widget.data.psy.base.psy[variable]
- if (sl is None or widget.plotmethod not in ['mapplot', 'plot2d'] or
- raw_data.ndim == 2 or
- widget.plotter.ax.figure.canvas.manager.toolbar.mode != ''):
+ if (
+ sl is None
+ or widget.plotmethod not in ["mapplot", "plot2d"]
+ or raw_data.ndim == 2
+ or widget.plotter.ax.figure.canvas.manager.toolbar.mode != ""
+ ):
return
# check if the mappable contains the event
if not self.plotter.plot.mappable.contains(event)[0] and (
- not hasattr(self.plotter.plot, '_wrapped_plot') or
- not self.plotter.plot._wrapped_plot.contains(event)[0]):
+ not hasattr(self.plotter.plot, "_wrapped_plot")
+ or not self.plotter.plot._wrapped_plot.contains(event)[0]
+ ):
return
current_pm = self.plotmethod
- self.plotmethod = 'lineplot'
+ self.plotmethod = "lineplot"
linewidget = self.plotmethod_widget
xdim = linewidget.xdim
if xdim is None:
xdim = self.combo_dims.currentText()
- if not linewidget.sp or (linewidget.xdim and
- linewidget.xdim not in raw_data.dims):
+ if not linewidget.sp or (
+ linewidget.xdim and linewidget.xdim not in raw_data.dims
+ ):
with self.silence_variable_buttons():
for v, btn in self.variable_buttons.items():
btn.setChecked(v == variable)
@@ -1371,7 +1469,6 @@ def display_line(self, event: MouseEvent) -> None:
linewidget.add_line(variable, **sl)
self.plotmethod = current_pm
-
def close_sp(self) -> None:
"""Close the current subproject."""
sp = self.sp
@@ -1442,7 +1539,8 @@ def reset_combo_array(self) -> None:
else:
idx_arr = 0
self.ds = list(
- all_arrays[idx_arr:idx_arr+1].datasets.values())[0]
+ all_arrays[idx_arr : idx_arr + 1].datasets.values()
+ )[0]
if self.ds is not current_ds:
with self.block_tree():
self.expand_ds_item(self.ds_item)
@@ -1477,7 +1575,6 @@ def refresh(self, reset_combo: bool = True) -> None:
self.btn_del.setEnabled(False)
self.btn_export.setEnabled(False)
-
# refresh variable buttons
with self.silence_variable_buttons():
for v, btn in self.variable_buttons.items():
@@ -1498,7 +1595,7 @@ def refresh(self, reset_combo: bool = True) -> None:
if self.sp:
data = self.data
- ds_data = self.ds[self.variable]
+ ds_data = self.ds.psy[self.variable]
with self.silence_variable_buttons():
self.variable_buttons[self.variable].setChecked(True)
@@ -1508,18 +1605,21 @@ def refresh(self, reset_combo: bool = True) -> None:
table.setVerticalHeaderLabels(ds_data.dims)
# set time, z, x, y info
- for c in 'XYTZ':
+ for c in "XYTZ":
cname = ds_data.psy.get_dim(c)
if cname and cname in dims:
table.setItem(
- dims.index(cname), 0, QtWidgets.QTableWidgetItem(c))
+ dims.index(cname), 0, QtWidgets.QTableWidgetItem(c)
+ )
for i, dim in enumerate(dims):
coord = self.ds[dim]
- if 'units' in coord.attrs:
+ if "units" in coord.attrs:
table.setItem(
- i, 4, QtWidgets.QTableWidgetItem(
- str(coord.attrs['units'])))
+ i,
+ 4,
+ QtWidgets.QTableWidgetItem(str(coord.attrs["units"])),
+ )
try:
coord = list(map("{:1.4f}".format, coord.values)) # type: ignore
except (ValueError, TypeError):
@@ -1531,22 +1631,21 @@ def refresh(self, reset_combo: bool = True) -> None:
coord = [t.isoformat() for t in coord] # type: ignore
first = coord[0]
last = coord[-1]
- table.setItem(
- i, 1, QtWidgets.QTableWidgetItem(first))
- table.setItem(
- i, 3, QtWidgets.QTableWidgetItem(last))
+ table.setItem(i, 1, QtWidgets.QTableWidgetItem(first))
+ table.setItem(i, 3, QtWidgets.QTableWidgetItem(last))
current = data.psy.idims.get(dim)
if current is not None and isinstance(current, int):
table.setCellWidget(
- i, 2, self.new_dimension_button(dim, coord[current]))
+ i, 2, self.new_dimension_button(dim, coord[current])
+ )
# update animation checkbox
dims_to_animate = get_dims_to_iterate(data)
- current_dims_to_animate = list(map(
- self.combo_dims.itemText,
- range(self.combo_dims.count())))
+ current_dims_to_animate = list(
+ map(self.combo_dims.itemText, range(self.combo_dims.count()))
+ )
if dims_to_animate != current_dims_to_animate:
self.combo_dims.clear()
self.combo_dims.addItems(dims_to_animate)
@@ -1560,16 +1659,20 @@ def refresh(self, reset_combo: bool = True) -> None:
if DatasetTree.is_variable(child):
plots_item = ds_item.get_plots_item(child)
ds_item.refresh_plots_item(
- plots_item, child.text(0), self._sp, self.sp)
+ plots_item, child.text(0), self._sp, self.sp
+ )
def new_dimension_button(
- self, dim: Hashable, label: Any) -> utils.QRightPushButton:
+ self, dim: Hashable, label: Any
+ ) -> utils.QRightPushButton:
"""Generate a new button to increase of decrease a dimension."""
btn = utils.QRightPushButton(label)
btn.clicked.connect(self.increase_dim(str(dim)))
btn.rightclicked.connect(self.increase_dim(str(dim), -1))
- btn.setToolTip(f"Increase dimension {dim} with left-click, and "
- "decrease with right-click.")
+ btn.setToolTip(
+ f"Increase dimension {dim} with left-click, and "
+ "decrease with right-click."
+ )
return btn
def update_project(self, *args, **kwargs) -> None:
@@ -1589,12 +1692,16 @@ def increase_dim(self, dim: str, increase: int = 1) -> Callable[[], None]:
increase: int
The number of steps to increase (or decrease) the given `dim`.
"""
+
def update():
i = self.data.psy.idims[dim]
- self.data.psy.update(dims={dim: (i+increase) % self.ds.dims[dim]})
+ self.data.psy.update(
+ dims={dim: (i + increase) % self.ds.dims[dim]}
+ )
if self.data.psy.plotter is None:
self.sp.update(replot=True)
self.refresh()
+
return update
@@ -1617,19 +1724,21 @@ class DatasetWidgetPlugin(DatasetWidget, DockMixin):
"""
#: The title of the widget
- title = 'psy-view Dataset viewer'
+ title = "psy-view Dataset viewer"
#: Display the dock widget at the right side of the GUI
dock_position = Qt.RightDockWidgetArea
def __init__(self, *args, **kwargs):
import psyplot.project as psy
+
super().__init__(*args, **kwargs)
psy.Project.oncpchange.connect(self.oncpchange)
@property # type: ignore
def _sp(self) -> Project: # type: ignore
import psyplot.project as psy
+
return psy.gcp(True)
@_sp.setter
@@ -1647,7 +1756,6 @@ def sp(self, sp: Optional[Project]):
if sp is None or not current:
return
elif getattr(current, self.plotmethod, []):
-
if len(current) == 1 and len(sp) == 1:
pass
# first remove the current arrays
@@ -1685,10 +1793,12 @@ def reload(self) -> None:
if not all(self._sp.dsnames_map.values()):
# we have datasets that only exist in memory, so better ask
answer = QtWidgets.QMessageBox.question(
- self, "Shall I close this?",
+ self,
+ "Shall I close this?",
"Reloading the data closes all open plots. Any data in the memory "
"is lost and open files are reloaded from disk! "
- "Shall I really continue?")
+ "Shall I really continue?",
+ )
if answer != QtWidgets.QMessageBox.Yes:
return
@@ -1722,6 +1832,7 @@ def oncpchange(self, sp: Optional[Project]) -> None:
def show_fig(self, sp: Optional[Project]) -> None:
"""Show the figure of the the current subproject."""
from psyplot_gui.main import mainwindow
+
super().show_fig(sp)
if mainwindow.figures and sp:
try:
@@ -1738,11 +1849,14 @@ def setup_ds_tree(self) -> None:
"""
self.ds_tree = tree = DatasetTree()
tree.setColumnCount(len(self.ds_attr_columns) + 1)
- tree.setHeaderLabels([''] + self.ds_attr_columns)
+ tree.setHeaderLabels([""] + self.ds_attr_columns)
def position_dock(self, main: MainWindow, *args, **kwargs) -> None:
height = main.help_explorer.dock.size().height()
main.splitDockWidget(main.help_explorer.dock, self.dock, Qt.Vertical)
- if hasattr(main, 'resizeDocks'): # qt >= 5.6
- main.resizeDocks([main.help_explorer.dock, self.dock],
- [height // 2, height // 2], Qt.Vertical)
+ if hasattr(main, "resizeDocks"): # qt >= 5.6
+ main.resizeDocks(
+ [main.help_explorer.dock, self.dock],
+ [height // 2, height // 2],
+ Qt.Vertical,
+ )
diff --git a/psy_view/icons/color_settings.png b/psy_view/icons/color_settings.png
index b986fca..c5950ac 100644
Binary files a/psy_view/icons/color_settings.png and b/psy_view/icons/color_settings.png differ
diff --git a/psy_view/icons/color_settings.png.license b/psy_view/icons/color_settings.png.license
new file mode 100644
index 0000000..eaac5c9
--- /dev/null
+++ b/psy_view/icons/color_settings.png.license
@@ -0,0 +1,4 @@
+SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH
+SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com)
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/psy_view/icons/color_settings.svg b/psy_view/icons/color_settings.svg
new file mode 100644
index 0000000..376af47
--- /dev/null
+++ b/psy_view/icons/color_settings.svg
@@ -0,0 +1 @@
+
diff --git a/psy_view/icons/color_settings.svg.license b/psy_view/icons/color_settings.svg.license
new file mode 100644
index 0000000..23f2891
--- /dev/null
+++ b/psy_view/icons/color_settings.svg.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com)
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/psy_view/icons/proj_settings.png b/psy_view/icons/proj_settings.png
index ef5af74..86838e1 100644
Binary files a/psy_view/icons/proj_settings.png and b/psy_view/icons/proj_settings.png differ
diff --git a/psy_view/icons/proj_settings.png.license b/psy_view/icons/proj_settings.png.license
new file mode 100644
index 0000000..eaac5c9
--- /dev/null
+++ b/psy_view/icons/proj_settings.png.license
@@ -0,0 +1,4 @@
+SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH
+SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com)
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/psy_view/icons/proj_settings.svg b/psy_view/icons/proj_settings.svg
new file mode 100644
index 0000000..1502b79
--- /dev/null
+++ b/psy_view/icons/proj_settings.svg
@@ -0,0 +1 @@
+
diff --git a/psy_view/icons/proj_settings.svg.license b/psy_view/icons/proj_settings.svg.license
new file mode 100644
index 0000000..23f2891
--- /dev/null
+++ b/psy_view/icons/proj_settings.svg.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com)
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/psy_view/plotmethods.py b/psy_view/plotmethods.py
index 97c1eb9..bf63441 100644
--- a/psy_view/plotmethods.py
+++ b/psy_view/plotmethods.py
@@ -4,72 +4,52 @@
lineplot plotmethods.
"""
-# Disclaimer
-# ----------
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
-#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
+# SPDX-License-Identifier: LGPL-3.0-only
from __future__ import annotations
-import os.path as osp
+import contextlib
+import dataclasses
+import textwrap
+from enum import Enum
+from functools import partial
+from itertools import chain, cycle
from typing import (
TYPE_CHECKING,
- ClassVar,
+ Any,
Callable,
- Optional,
- Union,
- List,
- Hashable,
+ ClassVar,
Dict,
- Any,
- Tuple,
+ Hashable,
Iterator,
- TypeVar,
+ List,
+ Optional,
+ Tuple,
Type,
+ TypeVar,
+ Union,
)
-from enum import Enum
-
-from functools import partial
-from itertools import chain, cycle
-import contextlib
-import textwrap
-
-import dataclasses
+import matplotlib.colors as mcol
+import psy_simple.widgets.colors as pswc
+import psyplot.data as psyd
import xarray as xr
from psyplot.utils import unique_everseen
+from psyplot_gui.common import get_icon as get_psy_icon
+from PyQt5 import QtCore, QtGui, QtWidgets
-from PyQt5 import QtWidgets, QtCore, QtGui
import psy_view.dialogs as dialogs
import psy_view.utils as utils
from psy_view.rcsetup import rcParams
-from psyplot_gui.common import get_icon as get_psy_icon
-import psy_simple.widgets.colors as pswc
-import matplotlib.colors as mcol
-
if TYPE_CHECKING:
- from xarray import DataArray, Dataset, Variable
- from psyplot.project import PlotterInterface, Project
from psyplot.data import InteractiveList
from psyplot.plotter import Plotter
+ from psyplot.project import Project
+ from xarray import DataArray, Dataset
T = TypeVar("T", bound="GridCell")
@@ -126,7 +106,7 @@ def from_alias(
o: Union[QtWidgets.QWidget, QtWidgets.QLayout],
c: Optional[int] = None,
cs: int = 1,
- s: bool = False
+ s: bool = False,
) -> T:
"""Create a :class:`GridCell` from shorter kws.
@@ -159,7 +139,7 @@ class PlotMethodWidget(QtWidgets.QWidget):
attribute.
"""
- plotmethod: ClassVar[str] = ''
+ plotmethod: ClassVar[str] = ""
#: trigger a replot of this widget. This can be emitted with the
#: :meth:`trigger_replot` method
@@ -178,8 +158,8 @@ class PlotMethodWidget(QtWidgets.QWidget):
layout: QtWidgets.QGridLayout = None
def __init__(
- self, get_sp: Callable[[], Optional[Project]],
- ds: Optional[Dataset]):
+ self, get_sp: Callable[[], Optional[Project]], ds: Optional[Dataset]
+ ):
super().__init__()
self._get_sp = get_sp
@@ -204,7 +184,6 @@ def formatoption_rows(self) -> List[List[GridCell]]:
rows.extend(self.get_rows(func))
return rows
-
def get_rows(self, func: Callable) -> List[List[GridCell]]:
"""Get the rows of the formatoption widgets.
@@ -249,6 +228,7 @@ def setup_widget_grid(self) -> None:
layout.addWidget(gc.qobject, i, col, 1, gc.colspan)
col += gc.colspan
layout.setRowStretch(len(rows), 1)
+
@property
def sp(self) -> Optional[Project]:
"""Get the subproject of this plotmethod interface."""
@@ -277,11 +257,12 @@ def formatoptions(self) -> List[str]:
return list(self.plotter)
else:
import psyplot.project as psy
+
return list(getattr(psy.plot, self.plotmethod).plotter_cls())
def get_fmts(
- self, var: DataArray, init: bool = False
- ) -> Dict[Union[Hashable, str, Any], Any]:
+ self, var: DataArray, init: bool = False
+ ) -> Dict[Union[Hashable, str, Any], Any]:
"""Get the formatoptions for a new plot.
Parameters
@@ -304,8 +285,8 @@ def get_fmts(
return ret
def init_dims(
- self, var: DataArray
- ) -> Dict[Union[Hashable, str, Any], Any]:
+ self, var: DataArray
+ ) -> Dict[Union[Hashable, str, Any], Any]:
"""Get the formatoptions for a new plot.
Parameters
@@ -332,8 +313,9 @@ def trigger_replot(self) -> None:
def trigger_reset(self):
"""Emit the :attr:`reset` signal to reinitialize the project."""
- self.array_info = self.sp.array_info(
- standardize_dims=False)[self.sp[0].psy.arr_name]
+ self.array_info = self.sp.array_info(standardize_dims=False)[
+ self.sp[0].psy.arr_name
+ ]
self.reset.emit(self.plotmethod)
def trigger_refresh(self):
@@ -341,8 +323,8 @@ def trigger_refresh(self):
self.changed.emit(self.plotmethod)
def get_slice(
- self, x: float, y: float
- ) -> Optional[Dict[Hashable, Union[int, slice]]]:
+ self, x: float, y: float
+ ) -> Optional[Dict[Hashable, Union[int, slice]]]:
"""Get the slice for the selected coordinates.
This method is called when the user clicks on the coordinates in the
@@ -375,8 +357,8 @@ def valid_variables(self, ds: Dataset) -> List[Hashable]:
plotmethod = getattr(ds.psy.plot, self.plotmethod)
for v in list(ds):
init_kws = self.init_dims(ds[v]) # type: ignore
- dims = init_kws.get('dims', {})
- decoder = init_kws.get('decoder')
+ dims = init_kws.get("dims", {})
+ decoder = init_kws.get("decoder")
if plotmethod.check_data(ds, v, dims, decoder)[0][0]:
ret.append(v)
return ret
@@ -384,6 +366,7 @@ def valid_variables(self, ds: Dataset) -> List[Hashable]:
class QHLine(QtWidgets.QFrame):
"""A horizontal seperation line."""
+
def __init__(self):
super().__init__()
self.setMinimumWidth(1)
@@ -391,15 +374,26 @@ def __init__(self):
self.setFrameShape(QtWidgets.QFrame.HLine)
self.setFrameShadow(QtWidgets.QFrame.Sunken)
self.setSizePolicy(
- QtWidgets.QSizePolicy.Preferred,
- QtWidgets.QSizePolicy.Minimum
+ QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum
)
class MapPlotWidget(PlotMethodWidget):
"""A widget to control the mapplot plotmethod."""
- plotmethod = 'mapplot'
+ plotmethod = "mapplot"
+
+ @property
+ def sp(self) -> Optional[Project]:
+ sp = super().sp
+ if sp:
+ arrays: List[str] = [
+ data.psy.arr_name
+ for data in sp
+ if not isinstance(data, psyd.InteractiveList)
+ ]
+ return sp(arr_name=arrays)
+ return sp
def get_rows(self, func: Callable) -> List[List[GridCell]]:
"""Get the rows of the formatoption widgets.
@@ -443,8 +437,10 @@ def fmt_setup_functions(self) -> List[Callable]:
a horizonal (or widget) that is added to the :attr:`layout` vbox.
"""
return [
- self.setup_color_buttons, self.setup_plot_buttons,
- self.setup_projection_buttons, self.setup_labels_button,
+ self.setup_color_buttons,
+ self.setup_plot_buttons,
+ self.setup_projection_buttons,
+ self.setup_labels_button,
self.setup_separation_line,
self.setup_dimension_box,
]
@@ -452,7 +448,9 @@ def fmt_setup_functions(self) -> List[Callable]:
def setup_labels_button(self) -> None:
"""Add a button to modify the text labels."""
self.btn_labels = utils.add_pushbutton(
- "Edit labels", self.edit_labels, "Edit title, colorbar labels, etc."
+ "Edit labels",
+ self.edit_labels,
+ "Edit title, colorbar labels, etc.",
)
def setup_plot_buttons(self) -> None:
@@ -463,18 +461,25 @@ def setup_plot_buttons(self) -> None:
PlotType.contourf,
PlotType.contour,
PlotType.poly,
- None
+ None,
]
self.combo_plot.setEditable(False)
- self.combo_plot.addItems([
- "Default", "Filled contours", "Contours", "Gridcell polygons",
- "Disable"
- ])
+ self.combo_plot.addItems(
+ [
+ "Default",
+ "Filled contours",
+ "Contours",
+ "Gridcell polygons",
+ "Disable",
+ ]
+ )
self.combo_plot.currentIndexChanged.connect(self._set_plot_type)
self.btn_datagrid = utils.add_pushbutton(
- "Gridcell boundaries", self.toggle_datagrid,
- "Toggle the visibility of grid cell boundaries")
+ "Gridcell boundaries",
+ self.toggle_datagrid,
+ "Toggle the visibility of grid cell boundaries",
+ )
self.btn_datagrid.setCheckable(True)
return
@@ -492,9 +497,11 @@ def setup_color_buttons(self) -> None:
self.setup_cmap_menu()
self.btn_cmap_settings = utils.add_pushbutton(
- utils.get_icon('color_settings'), self.edit_color_settings,
+ utils.get_icon("color_settings"),
+ self.edit_color_settings,
"Edit color settings",
- icon=True)
+ icon=True,
+ )
def setup_cmap_menu(self) -> QtWidgets.QMenu:
"""Set up the menu to change the colormaps."""
@@ -502,11 +509,14 @@ def setup_cmap_menu(self) -> QtWidgets.QMenu:
menu.addSeparator()
self.select_cmap_action = menu.addAction(
- 'More colormaps', self.open_cmap_dialog)
+ "More colormaps", self.open_cmap_dialog
+ )
self.color_settings_action = menu.addAction(
- QtGui.QIcon(utils.get_icon('color_settings')), 'More options',
- self.edit_color_settings)
+ QtGui.QIcon(utils.get_icon("color_settings")),
+ "More options",
+ self.edit_color_settings,
+ )
return menu
@@ -531,15 +541,17 @@ def open_cmap_dialog(self, N: int = 10) -> None:
def setup_projection_menu(self) -> QtWidgets.QMenu:
"""Set up the menu to modify the basemap."""
menu = QtWidgets.QMenu()
- for projection in rcParams['projections']:
+ for projection in rcParams["projections"]:
menu.addAction(
projection_map.get(projection, projection),
partial(self.set_projection, projection),
)
menu.addSeparator()
self.proj_settings_action = menu.addAction(
- QtGui.QIcon(utils.get_icon('proj_settings')),
- "Customize basemap...", self.edit_basemap_settings)
+ QtGui.QIcon(utils.get_icon("proj_settings")),
+ "Customize basemap...",
+ self.edit_basemap_settings,
+ )
return menu
def get_projection_label(self, proj: str) -> str:
@@ -593,8 +605,10 @@ def setup_projection_buttons(self) -> None:
"""Set up the buttons to modify the basemap."""
self.btn_proj = utils.add_pushbutton(
self.get_projection_label(rcParams["projections"][0]),
- self.choose_next_projection, "Change the basemap projection",
- toolbutton=True)
+ self.choose_next_projection,
+ "Change the basemap projection",
+ toolbutton=True,
+ )
self.btn_proj.setMenu(self.setup_projection_menu())
self.btn_proj.setSizePolicy(
@@ -603,27 +617,29 @@ def setup_projection_buttons(self) -> None:
self.btn_proj.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
self.btn_proj_settings = utils.add_pushbutton(
- utils.get_icon('proj_settings'), self.edit_basemap_settings,
+ utils.get_icon("proj_settings"),
+ self.edit_basemap_settings,
"Edit basemap settings",
- icon=True)
+ icon=True,
+ )
def setup_dimension_box(self) -> None:
"""Set up a box to control, what is the x and y-dimension."""
self.dimension_box = QtWidgets.QGridLayout()
- self.dimension_box.addWidget(QtWidgets.QLabel('x-Dimension:'), 0, 0)
+ self.dimension_box.addWidget(QtWidgets.QLabel("x-Dimension:"), 0, 0)
self.combo_xdim = QtWidgets.QComboBox()
self.dimension_box.addWidget(self.combo_xdim, 0, 1)
- self.dimension_box.addWidget(QtWidgets.QLabel('y-Dimension:'), 0, 2)
+ self.dimension_box.addWidget(QtWidgets.QLabel("y-Dimension:"), 0, 2)
self.combo_ydim = QtWidgets.QComboBox()
self.dimension_box.addWidget(self.combo_ydim, 0, 3)
- self.dimension_box.addWidget(QtWidgets.QLabel('x-Coordinate:'), 1, 0)
+ self.dimension_box.addWidget(QtWidgets.QLabel("x-Coordinate:"), 1, 0)
self.combo_xcoord = QtWidgets.QComboBox()
self.dimension_box.addWidget(self.combo_xcoord, 1, 1)
- self.dimension_box.addWidget(QtWidgets.QLabel('y-Coordinate:'), 1, 2)
+ self.dimension_box.addWidget(QtWidgets.QLabel("y-Coordinate:"), 1, 2)
self.combo_ycoord = QtWidgets.QComboBox()
self.dimension_box.addWidget(self.combo_ycoord, 1, 3)
@@ -655,8 +671,8 @@ def set_combo_text(self, combo: QtWidgets.QComboBox, text: str) -> None:
combo.setCurrentIndex(items.index(text))
def init_dims(
- self, var: DataArray
- ) -> Dict[Union[Hashable, str, Any], Any]:
+ self, var: DataArray
+ ) -> Dict[Union[Hashable, str, Any], Any]:
"""Get the formatoptions for a new plot.
This method updates the coordinates combo boxes with the
@@ -698,20 +714,23 @@ def init_dims(
else:
ydim = missing[-1]
dims[missing[-1]] = slice(None) # keep the last dimension
- ret['dims'] = dims
-
+ ret["dims"] = dims
if self.combo_xcoord.currentIndex():
xcoord = self.combo_xcoord.currentText()
- ret['decoder'] = {'x': {xcoord}}
+ ret["decoder"] = {"x": {xcoord}}
if self.combo_ycoord.currentIndex():
ycoord = self.combo_ycoord.currentText()
- ret.setdefault('decoder', {})
- ret['decoder']['y'] = {ycoord}
+ ret.setdefault("decoder", {})
+ ret["decoder"]["y"] = {ycoord}
- if (xdim is not None and xdim in var.dims and
- ydim is not None and ydim in var.dims):
- ret['transpose'] = var.dims.index(xdim) < var.dims.index(ydim)
+ if (
+ xdim is not None
+ and xdim in var.dims
+ and ydim is not None
+ and ydim in var.dims
+ ):
+ ret["transpose"] = var.dims.index(xdim) < var.dims.index(ydim)
return ret
@@ -729,8 +748,9 @@ def valid_variables(self, ds: Dataset) -> List[Hashable]:
List of variable names to plot
"""
valid = super().valid_variables(ds)
- if (not any(combo.count() for combo in self.coord_combos) or
- not any(combo.currentIndex() for combo in self.coord_combos)):
+ if not any(combo.count() for combo in self.coord_combos) or not any(
+ combo.currentIndex() for combo in self.coord_combos
+ ):
return valid
if self.combo_xdim.currentIndex():
xdim = self.combo_xdim.currentText()
@@ -740,19 +760,21 @@ def valid_variables(self, ds: Dataset) -> List[Hashable]:
valid = [v for v in valid if ydim in ds[v].dims]
if self.combo_xcoord.currentIndex():
xc_dims = set(ds[self.combo_xcoord.currentText()].dims)
- valid = [v for v in valid
- if xc_dims.intersection(ds[v].dims)]
+ valid = [v for v in valid if xc_dims.intersection(ds[v].dims)]
if self.combo_ycoord.currentIndex():
yc_dims = set(ds[self.combo_ycoord.currentText()].dims)
- valid = [v for v in valid
- if yc_dims.intersection(ds[v].dims)]
+ valid = [v for v in valid if yc_dims.intersection(ds[v].dims)]
return valid
@property
def coord_combos(self) -> List[QtWidgets.QComboBox]:
"""Get the combo boxes for x- and y-dimension and -coordinates."""
- return [self.combo_xdim, self.combo_ydim, self.combo_xcoord,
- self.combo_ycoord]
+ return [
+ self.combo_xdim,
+ self.combo_ydim,
+ self.combo_xcoord,
+ self.combo_ycoord,
+ ]
@contextlib.contextmanager
def block_combos(self) -> Iterator[None]:
@@ -787,21 +809,22 @@ def set_cmap(self, cmap: str) -> None:
The colormap name.
"""
plotter = self.plotter
- if plotter and 'cmap' in plotter:
+ if plotter and "cmap" in plotter:
plotter.update(cmap=cmap)
def toggle_datagrid(self) -> None:
"""Toggle the visibility of the grid cell boundaries."""
if self.plotter:
if self.btn_datagrid.isChecked():
- self.plotter.update(datagrid='k-')
+ self.plotter.update(datagrid="k-")
else:
self.plotter.update(datagrid=None)
def edit_labels(self) -> None:
"""Open the dialog to edit the text labels in the plot."""
dialogs.LabelDialog.update_project(
- self.sp, 'figtitle', 'title', 'clabel')
+ self.sp, "figtitle", "title", "clabel"
+ )
def edit_color_settings(self) -> None:
"""Open the dialog to edit the color settings."""
@@ -810,15 +833,15 @@ def edit_color_settings(self) -> None:
if isinstance(self.plotter.cmap.value, str):
self.btn_cmap.setText(self.plotter.cmap.value)
else:
- self.btn_cmap.setText('Custom')
+ self.btn_cmap.setText("Custom")
def choose_next_projection(self) -> None:
"""Choose the next projection from the rcParams `projection` value."""
select = False
- nprojections = len(rcParams['projections'])
+ nprojections = len(rcParams["projections"])
current = self.get_projection_value(self.btn_proj.text())
- for i, proj in enumerate(cycle(rcParams['projections'])):
+ for i, proj in enumerate(cycle(rcParams["projections"])):
if proj == current:
select = True
elif select or i == nprojections:
@@ -836,7 +859,7 @@ def set_projection(self, proj: str) -> None:
"""
self.btn_proj.setText(self.get_projection_label(proj))
plotter = self.plotter
- if plotter and 'projection' in plotter:
+ if plotter and "projection" in plotter:
plotter.update(projection=proj)
def _set_plot_type(self, i: int) -> None:
@@ -862,9 +885,8 @@ def edit_basemap_settings(self) -> None:
dialogs.BasemapDialog.update_plotter(self.plotter)
def get_fmts(
- self, var: DataArray,
- init: bool = False
- ) -> Dict[Union[Hashable, str, Any], Any]:
+ self, var: DataArray, init: bool = False
+ ) -> Dict[Union[Hashable, str, Any], Any]:
"""Get the formatoptions for a new plot.
Parameters
@@ -883,28 +905,28 @@ def get_fmts(
"""
fmts: Dict[Union[Hashable, str, Any], Any] = {}
- fmts['cmap'] = self.btn_cmap.text()
+ fmts["cmap"] = self.btn_cmap.text()
- if 'projection' in self.formatoptions:
- fmts['projection'] = self.get_projection_value(
+ if "projection" in self.formatoptions:
+ fmts["projection"] = self.get_projection_value(
self.btn_proj.text()
)
- if 'time' in var.dims:
- fmts['title'] = '%(time)s'
+ if "time" in var.dims:
+ fmts["title"] = "%(time)s"
- if 'long_name' in var.attrs:
- fmts['clabel'] = '%(long_name)s'
+ if "long_name" in var.attrs:
+ fmts["clabel"] = "%(long_name)s"
else:
- fmts['clabel'] = '%(name)s'
- if 'units' in var.attrs:
- fmts['clabel'] += ' %(units)s'
+ fmts["clabel"] = "%(name)s"
+ if "units" in var.attrs:
+ fmts["clabel"] += " %(units)s"
- fmts['plot'] = self.plot_types[self.combo_plot.currentIndex()]
- if fmts['plot'] == 'contour':
+ fmts["plot"] = self.plot_types[self.combo_plot.currentIndex()]
+ if fmts["plot"] == "contour":
# we need to set a global map extend, see
# https://github.com/SciTools/cartopy/issues/1673
- fmts['map_extent'] = 'global'
+ fmts["map_extent"] = "global"
if init:
fmts.update(self.init_dims(var))
@@ -915,29 +937,37 @@ def refresh(self, ds: Optional[Dataset]) -> None:
"""Refresh this widget from the given dataset."""
self.setEnabled(bool(self.sp))
- auto = 'Set automatically'
+ auto = "Set automatically"
self.refresh_from_sp()
with self.block_combos():
-
if ds is None:
ds = xr.Dataset()
- current_dims = set(map(
- self.combo_xdim.itemText, range(1, self.combo_xdim.count())))
+ current_dims = set(
+ map(
+ self.combo_xdim.itemText, range(1, self.combo_xdim.count())
+ )
+ )
ds_dims = list(
- map(str, (dim for dim, n in ds.dims.items() if n > 1)))
+ map(str, (dim for dim, n in ds.dims.items() if n > 1))
+ )
if current_dims != set(ds_dims):
self.combo_xdim.clear()
self.combo_ydim.clear()
self.combo_xdim.addItems([auto] + ds_dims)
self.combo_ydim.addItems([auto] + ds_dims)
- current_coords = set(map(
- self.combo_xcoord.itemText, range(1, self.combo_xcoord.count())))
+ current_coords = set(
+ map(
+ self.combo_xcoord.itemText,
+ range(1, self.combo_xcoord.count()),
+ )
+ )
ds_coords = list(
- map(str, (c for c, arr in ds.coords.items() if arr.ndim)))
+ map(str, (c for c, arr in ds.coords.items() if arr.ndim))
+ )
if current_coords != set(ds_coords):
self.combo_xcoord.clear()
self.combo_ycoord.clear()
@@ -947,8 +977,9 @@ def refresh(self, ds: Optional[Dataset]) -> None:
enable_combos = not bool(self.sp)
if not enable_combos and self.combo_xdim.isEnabled():
- self.reset_combos = [combo.currentIndex() == 0
- for combo in self.coord_combos]
+ self.reset_combos = [
+ combo.currentIndex() == 0 for combo in self.coord_combos
+ ]
elif enable_combos and not self.combo_xdim.isEnabled():
for reset, combo in zip(self.reset_combos, self.coord_combos):
if reset:
@@ -960,13 +991,13 @@ def refresh(self, ds: Optional[Dataset]) -> None:
if not enable_combos:
data = self.data
- xdim = str(data.psy.get_dim('x'))
- ydim = str(data.psy.get_dim('y'))
+ xdim = str(data.psy.get_dim("x"))
+ ydim = str(data.psy.get_dim("y"))
self.combo_xdim.setCurrentText(xdim)
self.combo_ydim.setCurrentText(ydim)
- xcoord = data.psy.get_coord('x')
+ xcoord = data.psy.get_coord("x")
xcoord = xcoord.name if xcoord is not None else xdim
- ycoord = data.psy.get_coord('y')
+ ycoord = data.psy.get_coord("y")
ycoord = ycoord.name if ycoord is not None else ydim
self.combo_xcoord.setCurrentText(xcoord)
@@ -981,11 +1012,11 @@ def refresh_from_sp(self) -> None:
self.get_projection_label(plotter.projection.value)
)
else:
- self.btn_proj.setText('Custom')
+ self.btn_proj.setText("Custom")
if isinstance(plotter.cmap.value, str):
self.btn_cmap.setText(plotter.cmap.value)
else:
- self.btn_cmap.setText('Custom')
+ self.btn_cmap.setText("Custom")
def transform(self, x: float, y: float) -> Tuple[float, float]:
"""Transform coordinates of a point to the plots projection.
@@ -1006,13 +1037,16 @@ def transform(self, x: float, y: float) -> Tuple[float, float]:
"""
import cartopy.crs as ccrs
import numpy as np
+
plotter = self.plotter
if not plotter:
raise ValueError(
"Cannot transform the coordinates as no plot is shown "
- "currently!")
+ "currently!"
+ )
x, y = plotter.transform.projection.transform_point(
- x, y, plotter.ax.projection)
+ x, y, plotter.ax.projection
+ )
# shift if necessary
if isinstance(plotter.transform.projection, ccrs.PlateCarree):
coord = plotter.plot.xcoord
@@ -1020,14 +1054,14 @@ def transform(self, x: float, y: float) -> Tuple[float, float]:
x -= 360
elif coord.max() <= 180 and x > 180:
x -= 360
- if 'rad' in coord.attrs.get('units', '').lower():
+ if "rad" in coord.attrs.get("units", "").lower():
x = np.deg2rad(x)
y = np.deg2rad(y)
return x, y
def get_slice(
- self, x: float, y: float
- ) -> Optional[Dict[Hashable, Union[int, slice]]]:
+ self, x: float, y: float
+ ) -> Optional[Dict[Hashable, Union[int, slice]]]:
"""Get the slice for the selected coordinates.
This method is called when the user clicks on the coordinates in the
@@ -1042,6 +1076,7 @@ def get_slice(
This is reimplemented in the :class:`MapPlotWidget`.
"""
import numpy as np
+
data = self.data.psy.base.psy[self.data.name]
x, y = self.transform(x, y)
plotter = self.plotter
@@ -1049,7 +1084,8 @@ def get_slice(
if not plotter:
raise ValueError(
"Cannot transform the coordinates as no plot is shown "
- "currently!")
+ "currently!"
+ )
fmto = plotter.plot
@@ -1061,20 +1097,19 @@ def get_slice(
imin = np.nanargmin(dist)
if xcoord.ndim == 2:
ncols = data.shape[-2]
- return dict(zip(data.dims[-2:],
- [imin // ncols, imin % ncols]))
+ return dict(zip(data.dims[-2:], [imin // ncols, imin % ncols]))
else:
return {data.dims[-1]: imin}
else:
- ix: int = xcoord.indexes[xcoord.name].get_loc(x, method='nearest')
- iy: int = ycoord.indexes[ycoord.name].get_loc(y, method='nearest')
+ ix: int = xcoord.indexes[xcoord.name].get_loc(x, method="nearest")
+ iy: int = ycoord.indexes[ycoord.name].get_loc(y, method="nearest")
return dict(zip(data.dims[-2:], [iy, ix]))
class Plot2DWidget(MapPlotWidget):
"""A widget to control the plot2d plotmethod."""
- plotmethod = 'plot2d'
+ plotmethod = "plot2d"
@property
def fmt_setup_functions(self) -> List[Callable]:
@@ -1102,7 +1137,8 @@ def setEnabled(self, b: bool) -> None:
def edit_labels(self) -> None:
"""Open the dialog to edit the text labels in the plot."""
dialogs.LabelDialog.update_project(
- self.sp, 'figtitle', 'title', 'clabel', 'xlabel', 'ylabel')
+ self.sp, "figtitle", "title", "clabel", "xlabel", "ylabel"
+ )
def transform(self, x: float, y: float) -> Tuple[float, float]:
"""Reimplemented to not transform the coordinates."""
@@ -1119,7 +1155,7 @@ def refresh_from_sp(self) -> None:
class LinePlotWidget(PlotMethodWidget):
"""A widget to control the lineplot plotmethod."""
- plotmethod = 'lineplot'
+ plotmethod = "lineplot"
@property
def fmt_setup_functions(self) -> List[Callable]:
@@ -1137,15 +1173,15 @@ def get_rows(self, func: Callable) -> List[List[GridCell]]:
"""
if func == self.setup_dimension_combo:
row = [
- GridCell(QtWidgets.QLabel('x-Dimension:')),
- GridCell(self.combo_dims)
+ GridCell(QtWidgets.QLabel("x-Dimension:")),
+ GridCell(self.combo_dims),
]
elif func == self.setup_line_selection:
row = [
- GridCell(QtWidgets.QLabel('Active line:')),
+ GridCell(QtWidgets.QLabel("Active line:")),
GridCell(self.combo_lines),
GridCell(self.btn_add),
- GridCell(self.btn_del)
+ GridCell(self.btn_del),
]
elif func == self.setup_labels_button:
row = [GridCell(self.btn_labels, colspan=4)]
@@ -1173,17 +1209,25 @@ def setup_line_selection(self) -> None:
self.combo_lines.currentIndexChanged.connect(self.trigger_refresh)
self.btn_add = utils.add_pushbutton(
- QtGui.QIcon(get_psy_icon('plus')), lambda: self.add_line(),
- "Add a line to the plot", icon=True)
+ QtGui.QIcon(get_psy_icon("plus")),
+ lambda: self.add_line(),
+ "Add a line to the plot",
+ icon=True,
+ )
self.btn_del = utils.add_pushbutton(
- QtGui.QIcon(get_psy_icon('minus')), self.remove_line,
- "Add a line to the plot", icon=True)
+ QtGui.QIcon(get_psy_icon("minus")),
+ self.remove_line,
+ "Add a line to the plot",
+ icon=True,
+ )
def setup_labels_button(self) -> None:
"""Add a button to modify the text labels."""
self.btn_labels = utils.add_pushbutton(
- "Edit labels", self.edit_labels,
- "Edit title, x-label, legendlabels, etc.")
+ "Edit labels",
+ self.edit_labels,
+ "Edit title, x-label, legendlabels, etc.",
+ )
@property
def xdim(self) -> str:
@@ -1223,9 +1267,11 @@ def add_line(self, name: Hashable = None, **sl) -> None:
xdim = self.xdim
if name is None:
name, ok = QtWidgets.QInputDialog.getItem(
- self, 'New line', 'Select a variable',
- [key for key, var in ds.items()
- if xdim in var.dims])
+ self,
+ "New line",
+ "Select a variable",
+ [key for key, var in ds.items() if xdim in var.dims],
+ )
if not ok:
return
arr = ds.psy[name]
@@ -1259,12 +1305,14 @@ def item_texts(self) -> List[str]:
"""Get the labels for the individual lines."""
if not self.sp:
return []
- return [f'Line {i}: {arr.psy._short_info()}'
- for i, arr in enumerate(self.sp[0])]
+ return [
+ f"Line {i}: {arr.psy._short_info()}"
+ for i, arr in enumerate(self.sp[0])
+ ]
def init_dims(
- self, var: DataArray
- ) -> Dict[Union[Hashable, str, Any], Any]:
+ self, var: DataArray
+ ) -> Dict[Union[Hashable, str, Any], Any]:
"""Get the formatoptions for a new plot.
Parameters
@@ -1286,16 +1334,16 @@ def init_dims(
if self.array_info:
arr_names = {}
for arrname, d in self.array_info.items():
- if arrname != 'attrs':
- dims = d['dims'].copy()
+ if arrname != "attrs":
+ dims = d["dims"].copy()
if xdim in dims:
for dim, sl in dims.items():
if not isinstance(sl, int):
dims[dim] = 0
dims[xdim] = slice(None)
- dims['name'] = d['name']
+ dims["name"] = d["name"]
arr_names[arrname] = dims
- ret['arr_names'] = arr_names
+ ret["arr_names"] = arr_names
del self.array_info
else:
if xdim not in var.dims:
@@ -1303,7 +1351,8 @@ def init_dims(
if xdim is None:
raise ValueError(
f"Cannot plot variable {var.name} with size smaller than "
- "2")
+ "2"
+ )
ret[xdim] = slice(None)
for d in var.dims:
if d != xdim:
@@ -1313,7 +1362,8 @@ def init_dims(
def edit_labels(self) -> None:
"""Open the dialog to edit the text labels in the plot."""
dialogs.LabelDialog.update_project(
- self.sp, 'figtitle', 'title', 'xlabel', 'ylabel', 'legendlabels')
+ self.sp, "figtitle", "title", "xlabel", "ylabel", "legendlabels"
+ )
@contextlib.contextmanager
def block_combos(self) -> Iterator[None]:
@@ -1363,20 +1413,29 @@ def refresh(self, ds) -> None:
if self.sp:
with self.block_combos():
self.combo_dims.clear()
- all_dims = list(chain.from_iterable(
- [[d for i, d in enumerate(a.dims) if a.shape[i] > 1]
- for a in arr.psy.iter_base_variables]
- for arr in self.sp[0]))
+ all_dims = list(
+ chain.from_iterable(
+ [
+ [d for i, d in enumerate(a.dims) if a.shape[i] > 1]
+ for a in arr.psy.iter_base_variables
+ ]
+ for arr in self.sp[0]
+ )
+ )
intersection = set(all_dims[0])
for dims in all_dims[1:]:
intersection.intersection_update(dims)
new_dims = list(
- filter(lambda d: d in intersection,
- unique_everseen(chain.from_iterable(all_dims))))
+ filter(
+ lambda d: d in intersection,
+ unique_everseen(chain.from_iterable(all_dims)),
+ )
+ )
self.combo_dims.addItems(new_dims)
self.combo_dims.setCurrentIndex(
- new_dims.index(self.data.dims[-1]))
+ new_dims.index(self.data.dims[-1])
+ )
# fill lines combo
current = self.combo_lines.currentIndex()
@@ -1385,7 +1444,9 @@ def refresh(self, ds) -> None:
short_descs = [textwrap.shorten(s, 50) for s in descriptions]
self.combo_lines.addItems(short_descs)
for i, desc in enumerate(descriptions):
- self.combo_lines.setItemData(i, desc, QtCore.Qt.ToolTipRole)
+ self.combo_lines.setItemData(
+ i, desc, QtCore.Qt.ToolTipRole
+ )
if current < len(descriptions):
self.combo_lines.setCurrentText(short_descs[current])
else:
diff --git a/psy_view/py.typed b/psy_view/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/psy_view/rcsetup.py b/psy_view/rcsetup.py
index 61c5152..c48b763 100644
--- a/psy_view/rcsetup.py
+++ b/psy_view/rcsetup.py
@@ -1,59 +1,46 @@
"""Configuration parameters for psy-view."""
-# Disclaimer
-# ----------
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
-#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
+# SPDX-License-Identifier: LGPL-3.0-only
from __future__ import annotations
-from typing import (
- Dict,
- List,
- Any,
- Optional,
-)
-from psyplot_gui.config.rcsetup import (
- RcParams, validate_stringlist, psyplot_fname)
-from psyplot.config.rcsetup import validate_dict
+from typing import Any, Dict, List, Optional
+from psyplot.config.rcsetup import validate_dict
+from psyplot_gui.config.rcsetup import (
+ RcParams,
+ psyplot_fname,
+ validate_stringlist,
+)
defaultParams: Dict[str, List[Any]] = {
"projections": [
["cf", "cyl", "robin", "ortho", "moll", "northpole", "southpole"],
- validate_stringlist, "The names of available projections"],
+ validate_stringlist,
+ "The names of available projections",
+ ],
"savefig_kws": [
- dict(dpi=250), validate_dict,
- "Options that are passed to plt.savefig when exporting images"],
+ dict(dpi=250),
+ validate_dict,
+ "Options that are passed to plt.savefig when exporting images",
+ ],
"animations.export_kws": [
- dict(writer="ffmpeg"), validate_dict,
- "Options that are passed to FuncAnimation.save"],
- }
+ dict(writer="ffmpeg"),
+ validate_dict,
+ "Options that are passed to FuncAnimation.save",
+ ],
+}
class PsyViewRcParams(RcParams):
"""RcParams for the psyplot-gui package."""
HEADER: str = RcParams.HEADER.replace(
- 'psyplotrc.yml', 'psyviewrc.yml').replace(
- 'PSYVIEWRC', 'psyviewrc.yml')
+ "psyplotrc.yml", "psyviewrc.yml"
+ ).replace("PSYVIEWRC", "psyviewrc.yml")
def load_from_file(self, fname: Optional[str] = None):
"""
@@ -71,8 +58,9 @@ def load_from_file(self, fname: Optional[str] = None):
See Also
--------
dump_to_file, psyplot_fname"""
- fname = fname or psyplot_fname(env_key='PSYVIEWRC',
- fname='psyviewrc.yml')
+ fname = fname or psyplot_fname(
+ env_key="PSYVIEWRC", fname="psyviewrc.yml"
+ )
if fname:
super().load_from_file(fname)
diff --git a/psy_view/utils.py b/psy_view/utils.py
index 9d71bcc..3214fc4 100644
--- a/psy_view/utils.py
+++ b/psy_view/utils.py
@@ -1,49 +1,35 @@
"""Utility functions for psy-view."""
-# Disclaimer
-# ----------
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
-#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
+# SPDX-License-Identifier: LGPL-3.0-only
from __future__ import annotations
-import os.path as osp
-from typing import Callable, Optional, Union, List, cast, TYPE_CHECKING
+import os.path as osp
+from typing import TYPE_CHECKING, Callable, List, Optional, Union, cast
-from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5 import QtCore, QtGui, QtWidgets
if TYPE_CHECKING:
from PyQt5.QtCore import QEvent # pylint: disable=no-name-in-module
-def get_icon(name: str, ending: str = '.png') -> str:
- return osp.join(osp.dirname(__file__), 'icons', name + ending)
+def get_icon(name: str, ending: str = ".png") -> str:
+ return osp.join(osp.dirname(__file__), "icons", name + ending)
def add_pushbutton(
- label: str,
- connections: Optional[Union[List[Callable], Callable]] = None,
- tooltip: Optional[str] = None,
- layout: Optional[QtWidgets.QLayout] = None,
- icon: bool = False, toolbutton: bool = False, *args, **kwargs
- ) -> Union[QtWidgets.QPushButton, QtWidgets.QToolButton]:
+ label: str,
+ connections: Optional[Union[List[Callable], Callable]] = None,
+ tooltip: Optional[str] = None,
+ layout: Optional[QtWidgets.QLayout] = None,
+ icon: bool = False,
+ toolbutton: bool = False,
+ *args,
+ **kwargs,
+) -> Union[QtWidgets.QPushButton, QtWidgets.QToolButton]:
if icon or toolbutton:
btn = QtWidgets.QToolButton(*args, **kwargs)
if icon:
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..6c69e27
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,166 @@
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: CC0-1.0
+
+[build-system]
+build-backend = 'setuptools.build_meta'
+requires = ['setuptools >= 61.0', 'versioneer[toml]']
+
+[project]
+name = "psy-view"
+dynamic = ["version"]
+description = "ncview-like interface to psyplot"
+
+readme = "README.md"
+keywords = [
+ "visualization",
+
+ "psyplot",
+
+ "netcdf",
+
+ "raster",
+
+ "cartopy",
+
+ "earth-sciences",
+
+ "pyqt",
+
+ "qt",
+
+ "ipython",
+
+ "jupyter",
+
+ "qtconsole",
+
+ "ncview",
+ ]
+
+authors = [
+ { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' },
+]
+maintainers = [
+ { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' },
+]
+license = { text = 'LGPL-3.0-only' }
+
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
+ "Topic :: Scientific/Engineering :: Visualization",
+ "Topic :: Scientific/Engineering :: GIS",
+ "Topic :: Scientific/Engineering",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Typing :: Typed",
+]
+
+requires-python = '>= 3.9'
+dependencies = [
+ "psyplot",
+ # add your dependencies here
+ "netCDF4",
+ "psyplot-gui>1.3.0",
+ "psy-maps>1.3.0",
+]
+
+[project.urls]
+Homepage = 'https://codebase.helmholtz.cloud/psyplot/psy-view'
+Documentation = "https://psyplot.github.io/psy-view"
+Source = "https://codebase.helmholtz.cloud/psyplot/psy-view"
+Tracker = "https://codebase.helmholtz.cloud/psyplot/psy-view/issues/"
+
+
+
+[project.scripts]
+psy-view = "psy_view:main"
+
+
+[project.entry-points."psyplot_gui"]
+psy-view = "psy_view.ds_widget:DatasetWidgetPlugin"
+
+[project.optional-dependencies]
+testsite = [
+ "tox",
+ "isort==5.12.0",
+ "black==23.1.0",
+ "blackdoc==0.3.8",
+ "flake8==6.0.0",
+ "pre-commit",
+ "mypy",
+ "pytest-cov",
+ "reuse",
+ "cffconvert",
+ "types-PyYAML",
+ "types-docutils",
+ "dask",
+ "pytest-qt",
+]
+docs = [
+ "autodocsumm",
+ "sphinx-rtd-theme",
+ "hereon-netcdf-sphinxext",
+ "sphinx-design",
+ "dask",
+ "sphinx-argparse",
+]
+dev = [
+ "psy-view[testsite]",
+ "psy-view[docs]",
+ "PyYAML",
+]
+
+
+[tool.mypy]
+ignore_missing_imports = true
+
+[tool.setuptools]
+zip-safe = false
+license-files = ["LICENSES/*"]
+
+[tool.setuptools.package-data]
+psy_view = [
+ "py.typed",
+ "psy_view/icons/*.png",
+ "psy_view/icons/*.png.license",
+]
+
+[tool.setuptools.packages.find]
+namespaces = false
+exclude = [
+ 'docs',
+ 'tests*',
+ 'examples'
+]
+
+[tool.pytest.ini_options]
+addopts = '-v'
+
+[tool.versioneer]
+VCS = 'git'
+style = 'pep440'
+versionfile_source = 'psy_view/_version.py'
+versionfile_build = 'psy_view/_version.py'
+tag_prefix = 'v'
+parentdir_prefix = 'psy-view-'
+
+[tool.isort]
+profile = "black"
+line_length = 79
+src_paths = ["psy_view"]
+float_to_top = true
+known_first_party = "psy_view"
+
+[tool.black]
+line-length = 79
+target-version = ['py39']
+
+[tool.coverage.run]
+omit = ["psy_view/_version.py"]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index effce11..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[versioneer]
-VCS = git
-style = pep440
-versionfile_source = psy_view/_version.py
-versionfile_build = psy_view/_version.py
-tag_prefix = v
-parentdir_prefix = psy-view-
diff --git a/setup.py b/setup.py
index ea6379f..06c91e2 100644
--- a/setup.py
+++ b/setup.py
@@ -1,125 +1,12 @@
-"""Setup script for the psy-view package."""
-
-# Disclaimer
-# ----------
-#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
-
-import os
-import os.path as osp
-from setuptools import setup, find_packages
-from setuptools.command.test import test as TestCommand
-import sys
+# SPDX-License-Identifier: CC0-1.0
+"""Setup script for the psy-view package."""
import versioneer
+from setuptools import setup
-
-class PyTest(TestCommand):
- user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")]
-
- def initialize_options(self):
- TestCommand.initialize_options(self)
- self.pytest_args = ''
-
- def run_tests(self):
- import shlex
- # import here, cause outside the eggs aren't loaded
- import pytest
- errno = pytest.main(shlex.split(self.pytest_args))
- sys.exit(errno)
-
-
-def readme():
- with open('README.rst') as f:
- return f.read().replace(
- 'docs/_static/screenshot.png',
- 'https://raw.githubusercontent.com/psyplot/psy-view/master/'
- 'docs/_static/screenshot.png')
-
-
-version = versioneer.get_version()
-
-dependencies = [
- 'psyplot-gui>=1.3.0',
- 'psy-maps>=1.3.0',
- 'netCDF4',
-]
-
-# Test for PyQt5 dependency. During a conda build, this is handled by the
-# meta.yaml so we can skip this dependency
-if not os.getenv('CONDA_BUILD'):
- # The package might nevertheless be installed, just registered with a
- # different name
- try:
- import PyQt5
- except ImportError:
- dependencies.append('pyqt5!=5.12')
- dependencies.append('PyQtWebEngine')
- dependencies.append('pyqt5-sip')
-
-
-cmdclass = versioneer.get_cmdclass({'test': PyTest})
-
-
-setup(name='psy-view',
- version=version,
- description='ncview-like interface to psyplot',
- long_description=readme(),
- long_description_content_type="text/x-rst",
- classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Science/Research',
- 'Topic :: Scientific/Engineering :: Visualization',
- 'Topic :: Scientific/Engineering :: GIS',
- 'Topic :: Scientific/Engineering',
- 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8',
- 'Programming Language :: Python :: 3.9',
- 'Operating System :: OS Independent',
- ],
- keywords=(
- 'visualization netcdf raster cartopy earth-sciences pyqt qt '
- 'ipython jupyter qtconsole ncview'
- ),
- url='https://github.com/psyplot/psy-view',
- author='Philipp S. Sommer',
- author_email='psyplot@hereon.de',
- license="LGPL-3.0-only",
- packages=find_packages(exclude=['docs', 'tests*', 'examples']),
- install_requires=dependencies,
- package_data={'psy_view': [
- osp.join('psy_view', 'icons', '*.png'),
- ]},
- include_package_data=True,
- tests_require=['pytest', 'pytest-qt'],
- cmdclass=cmdclass,
- entry_points={
- 'console_scripts': ['psy-view=psy_view:main'],
- 'psyplot_gui': ['psy-view=psy_view.ds_widget:DatasetWidgetPlugin'],
- },
- project_urls={
- 'Documentation': 'https://psyplot.github.io/psy-view',
- 'Source': 'https://github.com/psyplot/psy-viewi',
- 'Tracker': 'https://github.com/psyplot/psy-view/issues',
- },
- zip_safe=False)
+setup(
+ version=versioneer.get_version(),
+ cmdclass=versioneer.get_cmdclass(),
+)
diff --git a/tests/conftest.py b/tests/conftest.py
index 1d1897a..ca0944c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,31 +1,15 @@
"""pytest configuration file for psy-view."""
-# Disclaimer
-# ----------
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
-#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
+# SPDX-License-Identifier: LGPL-3.0-only
import os.path as osp
-import pytest
-import psyplot_gui.compat.qtcompat
+# import psyplot_gui.compat to make sure, qt settings are set
+import psyplot_gui.compat.qtcompat # noqa: F401
+import pytest
_test_dir = osp.dirname(__file__)
@@ -35,8 +19,14 @@ def test_dir() -> str:
return _test_dir
-@pytest.fixture(params=["regular-test.nc", "regional-icon-test.nc",
- "rotated-pole-test.nc", "icon-test.nc"])
+@pytest.fixture(
+ params=[
+ "regular-test.nc",
+ "regional-icon-test.nc",
+ "rotated-pole-test.nc",
+ "icon-test.nc",
+ ]
+)
def test_file(test_dir, request):
return osp.join(test_dir, request.param)
@@ -44,18 +34,21 @@ def test_file(test_dir, request):
@pytest.fixture
def test_ds(test_file):
import psyplot.data as psyd
+
with psyd.open_dataset(test_file) as ds:
yield ds
@pytest.fixture
def ds_widget(qtbot, test_ds):
- import psyplot.project as psy
import matplotlib.pyplot as plt
+ import psyplot.project as psy
+
from psy_view.ds_widget import DatasetWidget
+
w = DatasetWidget(test_ds)
qtbot.addWidget(w)
yield w
w._sp = None
- psy.close('all')
- plt.close('all')
+ psy.close("all")
+ plt.close("all")
diff --git a/tests/icon-test.nc.license b/tests/icon-test.nc.license
new file mode 100644
index 0000000..919c9c1
--- /dev/null
+++ b/tests/icon-test.nc.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+
+SPDX-License-Identifier: CC0-1.0
diff --git a/tests/pytest.ini b/tests/pytest.ini
index 3becfac..c2c952e 100644
--- a/tests/pytest.ini
+++ b/tests/pytest.ini
@@ -1,3 +1,6 @@
+; SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+;
+; SPDX-License-Identifier: CC-BY-4.0
+
[pytest]
qt_api=pyqt5
-
diff --git a/tests/regional-icon-test.nc.license b/tests/regional-icon-test.nc.license
new file mode 100644
index 0000000..919c9c1
--- /dev/null
+++ b/tests/regional-icon-test.nc.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+
+SPDX-License-Identifier: CC0-1.0
diff --git a/tests/regular-test.nc.license b/tests/regular-test.nc.license
new file mode 100644
index 0000000..919c9c1
--- /dev/null
+++ b/tests/regular-test.nc.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+
+SPDX-License-Identifier: CC0-1.0
diff --git a/tests/rotated-pole-test.nc.license b/tests/rotated-pole-test.nc.license
new file mode 100644
index 0000000..919c9c1
--- /dev/null
+++ b/tests/rotated-pole-test.nc.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+
+SPDX-License-Identifier: CC0-1.0
diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py
index b688cf1..5cd9d5a 100644
--- a/tests/test_dialogs.py
+++ b/tests/test_dialogs.py
@@ -1,33 +1,16 @@
"""Test the formatoption dialogs."""
-# Disclaimer
-# ----------
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
-#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
+# SPDX-License-Identifier: LGPL-3.0-only
import pytest
@pytest.fixture
def test_project(test_ds):
- sp = test_ds.psy.plot.mapplot(name='t2m')
+ sp = test_ds.psy.plot.mapplot(name="t2m")
yield sp
sp.close()
@@ -35,6 +18,7 @@ def test_project(test_ds):
@pytest.fixture
def cmap_dialog(qtbot, test_project):
from psy_view.dialogs import CmapDialog
+
dialog = CmapDialog(test_project)
qtbot.addWidget(dialog)
return dialog
@@ -50,7 +34,7 @@ def test_colorbar_preview_valid_bounds(cmap_dialog):
def test_colorbar_preview_valid_cmap(cmap_dialog):
"""Test whether the update to a new cmap setting works"""
- cmap = 'Blues'
+ cmap = "Blues"
cmap_dialog.cmap_widget.editor.set_obj(cmap)
assert cmap_dialog.cbar_preview.cbar.cmap.name == cmap
@@ -69,7 +53,7 @@ def test_colorbar_preview_invalid_bounds(cmap_dialog):
bounds = list(cmap_dialog.cbar_preview.cbar.norm.boundaries)
# set invalid bounds
- cmap_dialog.bounds_widget.editor.text = '[1, 2, 3'
+ cmap_dialog.bounds_widget.editor.text = "[1, 2, 3"
assert list(cmap_dialog.cbar_preview.cbar.norm.boundaries) == bounds
@@ -79,7 +63,7 @@ def test_colorbar_preview_invalid_cmap(cmap_dialog):
cmap = cmap_dialog.cbar_preview.cbar.cmap.name
# set invalid cmap
- cmap_dialog.cmap_widget.editor.text = 'Blue'
+ cmap_dialog.cmap_widget.editor.text = "Blue"
assert cmap_dialog.cbar_preview.cbar.cmap.name == cmap
@@ -89,7 +73,7 @@ def test_colorbar_preview_invalid_ticks(cmap_dialog):
ticks = list(cmap_dialog.cbar_preview.cbar.get_ticks())
# set invalid ticks
- cmap_dialog.cticks_widget.editor.text = '[1, 2, 3'
+ cmap_dialog.cticks_widget.editor.text = "[1, 2, 3"
assert list(cmap_dialog.cbar_preview.cbar.get_ticks()) == ticks
@@ -98,6 +82,6 @@ def test_cmap_dialog_fmts(cmap_dialog):
"""Test the updating of formatoptions"""
assert not cmap_dialog.fmts
- cmap_dialog.bounds_widget.editor.set_obj('minmax')
+ cmap_dialog.bounds_widget.editor.set_obj("minmax")
- assert cmap_dialog.fmts == {'bounds': 'minmax'}
\ No newline at end of file
+ assert cmap_dialog.fmts == {"bounds": "minmax"}
diff --git a/tests/test_ds_widget.py b/tests/test_ds_widget.py
index 25f3c11..77a264a 100644
--- a/tests/test_ds_widget.py
+++ b/tests/test_ds_widget.py
@@ -1,33 +1,17 @@
"""Test the main functionality of the psy-view package, namely the widget."""
-# Disclaimer
-# ----------
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
-#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
+# SPDX-License-Identifier: LGPL-3.0-only
import os.path as osp
-import sys
import shutil
-from PyQt5.QtCore import Qt
-from PyQt5 import QtWidgets
+import sys
+
import pytest
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import Qt
def test_variables(ds_widget, test_ds):
@@ -39,19 +23,19 @@ def test_variables(ds_widget, test_ds):
def test_mapplot(qtbot, ds_widget):
"""Test plotting and closing with mapplot"""
- ds_widget.plotmethod = 'mapplot'
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ ds_widget.plotmethod = "mapplot"
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert ds_widget.sp
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert not ds_widget.sp
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d'])
-@pytest.mark.parametrize('i', list(range(5)))
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"])
+@pytest.mark.parametrize("i", list(range(5)))
def test_change_plot_type(qtbot, ds_widget, plotmethod, i):
"""Test plotting and closing with mapplot"""
ds_widget.plotmethod = plotmethod
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert ds_widget.sp
pm_widget = ds_widget.plotmethod_widget
pm_widget.combo_plot.setCurrentIndex(i)
@@ -60,39 +44,39 @@ def test_change_plot_type(qtbot, ds_widget, plotmethod, i):
assert ds_widget.sp.plotters[0].plot.value == plot_type
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d'])
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"])
def test_variable_switch(qtbot, ds_widget, plotmethod):
"""Test switching of variables"""
ds_widget.plotmethod = plotmethod
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert len(ds_widget.sp) == 1
- assert ds_widget.data.name == 't2m'
- qtbot.mouseClick(ds_widget.variable_buttons['v'], Qt.LeftButton)
+ assert ds_widget.data.name == "t2m"
+ qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton)
assert len(ds_widget.sp) == 1
- assert ds_widget.data.name == 'v'
- qtbot.mouseClick(ds_widget.variable_buttons['v_2d'], Qt.LeftButton)
+ assert ds_widget.data.name == "v"
+ qtbot.mouseClick(ds_widget.variable_buttons["v_2d"], Qt.LeftButton)
assert len(ds_widget.sp) == 1
- assert ds_widget.data.name == 'v_2d'
- qtbot.mouseClick(ds_widget.variable_buttons['v'], Qt.LeftButton)
+ assert ds_widget.data.name == "v_2d"
+ qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton)
assert len(ds_widget.sp) == 1
- assert ds_widget.data.name == 'v'
- qtbot.mouseClick(ds_widget.variable_buttons['v'], Qt.LeftButton)
+ assert ds_widget.data.name == "v"
+ qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton)
assert not ds_widget.sp
def test_plot2d(qtbot, ds_widget):
"""Test plotting and closing with plot2d"""
- ds_widget.plotmethod = 'plot2d'
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ ds_widget.plotmethod = "plot2d"
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert ds_widget.sp
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert not ds_widget.sp
def test_plot2d_dim_switch(qtbot, ds_widget, test_ds, test_file):
- arr = test_ds['t2m']
+ arr = test_ds["t2m"]
- ds_widget.plotmethod = 'plot2d'
+ ds_widget.plotmethod = "plot2d"
pm_widget = ds_widget.plotmethod_widget
@@ -104,9 +88,9 @@ def test_plot2d_dim_switch(qtbot, ds_widget, test_ds, test_file):
fmts = pm_widget.init_dims(arr)
- assert fmts['transpose']
+ assert fmts["transpose"]
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert not pm_widget.combo_xdim.isEnabled()
@@ -114,19 +98,19 @@ def test_plot2d_dim_switch(qtbot, ds_widget, test_ds, test_file):
assert ds_widget.plotter.plot_data.dims == arr.dims[:2]
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d'])
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"])
def test_plot2d_coord(qtbot, ds_widget, test_ds, test_file, plotmethod):
- arr = test_ds.psy['t2m']
+ arr = test_ds.psy["t2m"]
if osp.basename(test_file) != "rotated-pole-test.nc":
return pytest.skip("Testing rotated coords only")
ydim, xdim = arr.dims[-2:]
- test_ds[xdim].attrs.pop('axis', None)
- test_ds[ydim].attrs.pop('axis', None)
+ test_ds[xdim].attrs.pop("axis", None)
+ test_ds[ydim].attrs.pop("axis", None)
- assert 'coordinates' in arr.encoding
+ assert "coordinates" in arr.encoding
ds_widget.plotmethod = plotmethod
@@ -135,15 +119,15 @@ def test_plot2d_coord(qtbot, ds_widget, test_ds, test_file, plotmethod):
assert pm_widget.combo_xcoord.isEnabled()
# make the plot with default setting
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert not pm_widget.combo_xcoord.isEnabled()
- assert pm_widget.data.psy.get_coord('x').name != xdim
- assert pm_widget.data.psy.get_coord('y').name != ydim
+ assert pm_widget.data.psy.get_coord("x").name != xdim
+ assert pm_widget.data.psy.get_coord("y").name != ydim
# remove the plot
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert pm_widget.combo_xcoord.isEnabled()
@@ -152,72 +136,72 @@ def test_plot2d_coord(qtbot, ds_widget, test_ds, test_file, plotmethod):
pm_widget.combo_ycoord.setCurrentText(ydim)
# make the plot with the changed settings
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert not pm_widget.combo_xcoord.isEnabled()
- assert pm_widget.data.psy.get_coord('x').name == xdim
- assert pm_widget.data.psy.get_coord('y').name == ydim
+ assert pm_widget.data.psy.get_coord("x").name == xdim
+ assert pm_widget.data.psy.get_coord("y").name == ydim
def test_lineplot(qtbot, ds_widget):
"""Test plotting and closing with lineplot"""
- ds_widget.plotmethod = 'lineplot'
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ ds_widget.plotmethod = "lineplot"
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert ds_widget.sp
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert not ds_widget.sp
def test_lineplot_switch(qtbot, ds_widget):
"""Test switching of variables"""
- ds_widget.plotmethod = 'lineplot'
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ ds_widget.plotmethod = "lineplot"
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert len(ds_widget.sp) == 1
- assert ds_widget.data.name == 't2m'
- qtbot.mouseClick(ds_widget.variable_buttons['v'], Qt.LeftButton)
+ assert ds_widget.data.name == "t2m"
+ qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton)
assert len(ds_widget.sp) == 1
- assert ds_widget.data.name == 'v'
- qtbot.mouseClick(ds_widget.variable_buttons['v'], Qt.LeftButton)
+ assert ds_widget.data.name == "v"
+ qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton)
assert not ds_widget.sp
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d'])
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"])
def test_cmap(qtbot, ds_widget, plotmethod):
ds_widget.plotmethod = plotmethod
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
cmap = ds_widget.plotter.cmap.value
assert ds_widget.plotter.plot.mappable.get_cmap().name == cmap
ds_widget.plotmethod_widget.btn_cmap.menu().actions()[5].trigger()
assert ds_widget.plotter.cmap.value != cmap
assert ds_widget.plotter.plot.mappable.get_cmap().name != cmap
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
def test_add_and_remove_line(qtbot, ds_widget, monkeypatch):
"Test adding and removing lines"
- ds_widget.plotmethod = 'lineplot'
+ ds_widget.plotmethod = "lineplot"
monkeypatch.setattr(
- QtWidgets.QInputDialog, "getItem",
- lambda *args: ('t2m', True))
+ QtWidgets.QInputDialog, "getItem", lambda *args: ("t2m", True)
+ )
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert ds_widget.sp
assert len(ds_widget.sp[0]) == 1
qtbot.mouseClick(ds_widget.plotmethod_widget.btn_add, Qt.LeftButton)
assert len(ds_widget.sp[0]) == 2
qtbot.mouseClick(ds_widget.plotmethod_widget.btn_del, Qt.LeftButton)
assert len(ds_widget.sp[0]) == 1
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert not ds_widget.sp
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d', 'lineplot'])
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"])
def test_btn_step(qtbot, ds_widget, plotmethod):
"""Test clicking the next time button"""
ds_widget.plotmethod = plotmethod
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
dim = ds_widget.combo_dims.currentText()
assert dim
assert ds_widget.data.psy.idims[dim] == 0
@@ -230,11 +214,12 @@ def test_btn_step(qtbot, ds_widget, plotmethod):
qtbot.mouseClick(ds_widget.btn_prev, Qt.LeftButton)
assert ds_widget.data.psy.idims[dim] == 0
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d', 'lineplot'])
+
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"])
def test_dimension_button(qtbot, ds_widget, plotmethod):
"""Test clicking on a button in the dimension table"""
ds_widget.plotmethod = plotmethod
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
btn = ds_widget.dimension_table.cellWidget(1, 2)
@@ -251,8 +236,8 @@ def test_dimension_button(qtbot, ds_widget, plotmethod):
assert ds_widget.data.psy.idims[dim] == 0
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d', 'lineplot'])
-@pytest.mark.parametrize('direction', ['forward', 'backward'])
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"])
+@pytest.mark.parametrize("direction", ["forward", "backward"])
def test_animate(qtbot, ds_widget, plotmethod, direction):
"""Test clicking the next time button"""
@@ -266,17 +251,16 @@ def animation_finished():
else:
return True
-
ds_widget.plotmethod = plotmethod
ds_widget.sl_interval.setValue(10)
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
dim = ds_widget.combo_dims.currentText()
assert dim
steps = set(range(ds_widget.ds.dims[dim]))
- btn = getattr(ds_widget, 'btn_animate_' + direction)
+ btn = getattr(ds_widget, "btn_animate_" + direction)
assert not ds_widget._animating
@@ -301,46 +285,47 @@ def animation_finished():
def test_enable_disable_variables(test_ds, qtbot):
- from psy_view.ds_widget import DatasetWidget
import numpy as np
- test_ds['line'] = ('xtest', np.zeros(7))
- test_ds['xtest'] = ('xtest', np.arange(7))
+
+ from psy_view.ds_widget import DatasetWidget
+
+ test_ds["line"] = ("xtest", np.zeros(7))
+ test_ds["xtest"] = ("xtest", np.arange(7))
ds_widget = DatasetWidget(test_ds)
qtbot.addWidget(ds_widget)
- assert ds_widget.variable_buttons['t2m'].isEnabled()
- assert not ds_widget.variable_buttons['line'].isEnabled()
+ assert ds_widget.variable_buttons["t2m"].isEnabled()
+ assert not ds_widget.variable_buttons["line"].isEnabled()
- ds_widget.plotmethod = 'lineplot'
+ ds_widget.plotmethod = "lineplot"
- assert ds_widget.variable_buttons['t2m'].isEnabled()
- assert ds_widget.variable_buttons['line'].isEnabled()
+ assert ds_widget.variable_buttons["t2m"].isEnabled()
+ assert ds_widget.variable_buttons["line"].isEnabled()
- ds_widget.plotmethod = 'plot2d'
+ ds_widget.plotmethod = "plot2d"
- assert ds_widget.variable_buttons['t2m'].isEnabled()
- assert not ds_widget.variable_buttons['line'].isEnabled()
+ assert ds_widget.variable_buttons["t2m"].isEnabled()
+ assert not ds_widget.variable_buttons["line"].isEnabled()
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d', 'lineplot'])
-def test_open_and_close_plots(
- ds_widget, qtbot, monkeypatch, plotmethod):
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"])
+def test_open_and_close_plots(ds_widget, qtbot, monkeypatch, plotmethod):
"""Create multiple plots and export them all"""
ds_widget.plotmethod = plotmethod
monkeypatch.setattr(
- QtWidgets.QInputDialog, "getItem",
- lambda *args: ('t2m', True))
+ QtWidgets.QInputDialog, "getItem", lambda *args: ("t2m", True)
+ )
qtbot.mouseClick(ds_widget.btn_add, Qt.LeftButton)
assert ds_widget.sp
assert len(ds_widget.sp) == 1
- assert ds_widget.variable_buttons['t2m'].isChecked()
+ assert ds_widget.variable_buttons["t2m"].isChecked()
monkeypatch.setattr(
- QtWidgets.QInputDialog, "getItem",
- lambda *args: ('u', True))
+ QtWidgets.QInputDialog, "getItem", lambda *args: ("u", True)
+ )
# create a second plot
qtbot.mouseClick(ds_widget.btn_add, Qt.LeftButton)
@@ -350,42 +335,43 @@ def test_open_and_close_plots(
assert len(ds_widget._sp) == 2
assert ds_widget.combo_array.count() == 2
assert ds_widget.combo_array.currentIndex() == 1
- assert ds_widget.variable_buttons['u'].isChecked()
+ assert ds_widget.variable_buttons["u"].isChecked()
# switch to the first variable
ds_widget.combo_array.setCurrentIndex(0)
assert len(ds_widget.sp) == 1
assert len(ds_widget._sp) == 2
- assert ds_widget.data.name == 't2m'
- assert ds_widget.variable_buttons['t2m'].isChecked()
+ assert ds_widget.data.name == "t2m"
+ assert ds_widget.variable_buttons["t2m"].isChecked()
# close the plot
qtbot.mouseClick(ds_widget.btn_del, Qt.LeftButton)
assert len(ds_widget.sp) == 1
assert len(ds_widget._sp) == 1
- assert ds_widget.data.name == 'u'
- assert ds_widget.variable_buttons['u'].isChecked()
+ assert ds_widget.data.name == "u"
+ assert ds_widget.variable_buttons["u"].isChecked()
# close the second plot
qtbot.mouseClick(ds_widget.btn_del, Qt.LeftButton)
assert not bool(ds_widget.sp)
assert not bool(ds_widget._sp)
- assert not any(btn.isChecked() for name, btn in
- ds_widget.variable_buttons.items())
+ assert not any(
+ btn.isChecked() for name, btn in ds_widget.variable_buttons.items()
+ )
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d', 'lineplot'])
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"])
def test_multi_export(ds_widget, qtbot, monkeypatch, tmpdir, plotmethod):
"""Create multiple plots and export them all"""
ds_widget.plotmethod = plotmethod
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert ds_widget.sp
assert len(ds_widget.sp) == 1
monkeypatch.setattr(
- QtWidgets.QInputDialog, "getItem",
- lambda *args: ('u', True))
+ QtWidgets.QInputDialog, "getItem", lambda *args: ("u", True)
+ )
# create a second plot
qtbot.mouseClick(ds_widget.btn_add, Qt.LeftButton)
@@ -399,16 +385,20 @@ def test_multi_export(ds_widget, qtbot, monkeypatch, tmpdir, plotmethod):
# export the plots
monkeypatch.setattr(
- QtWidgets.QFileDialog, "getSaveFileName",
- lambda *args: (osp.join(tmpdir, "test.pdf"), True))
+ QtWidgets.QFileDialog,
+ "getSaveFileName",
+ lambda *args: (osp.join(tmpdir, "test.pdf"), True),
+ )
ds_widget.export_all_images()
# Test if warning is triggered when exporting only one image
monkeypatch.setattr(
- QtWidgets.QFileDialog, "getSaveFileName",
- lambda *args: (osp.join(tmpdir, "test.png"), True))
+ QtWidgets.QFileDialog,
+ "getSaveFileName",
+ lambda *args: (osp.join(tmpdir, "test.png"), True),
+ )
question_asked = []
@@ -416,10 +406,7 @@ def dont_save(*args):
question_asked.append(True)
return QtWidgets.QMessageBox.No
- monkeypatch.setattr(
- QtWidgets.QMessageBox, "question", dont_save)
-
-
+ monkeypatch.setattr(QtWidgets.QMessageBox, "question", dont_save)
ds_widget.export_all_images()
@@ -428,14 +415,14 @@ def dont_save(*args):
assert not osp.exists(osp.join(tmpdir, "test.png"))
-@pytest.mark.parametrize('plotmethod', ['mapplot', 'plot2d', 'lineplot'])
+@pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"])
def test_export_animation(qtbot, ds_widget, plotmethod, tmpdir, monkeypatch):
"""Test clicking the next time button"""
from psy_view.rcsetup import rcParams
ds_widget.plotmethod = plotmethod
ds_widget.sl_interval.setValue(10)
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
dim = ds_widget.combo_dims.currentText()
assert dim
@@ -443,11 +430,13 @@ def test_export_animation(qtbot, ds_widget, plotmethod, tmpdir, monkeypatch):
assert not ds_widget._animating
monkeypatch.setattr(
- QtWidgets.QFileDialog, "getSaveFileName",
- lambda *args: (osp.join(tmpdir, "test.gif"), True))
+ QtWidgets.QFileDialog,
+ "getSaveFileName",
+ lambda *args: (osp.join(tmpdir, "test.gif"), True),
+ )
with rcParams.catch():
- rcParams['animations.export_kws'] = {'writer': 'pillow'}
+ rcParams["animations.export_kws"] = {"writer": "pillow"}
ds_widget.export_animation()
@@ -460,6 +449,7 @@ def test_export_animation(qtbot, ds_widget, plotmethod, tmpdir, monkeypatch):
def test_reload(qtbot, test_dir, tmp_path) -> None:
"""Test the reload button."""
import psyplot.project as psy
+
from psy_view.ds_widget import DatasetWidget
f1, f2 = "regular-test.nc", "regional-icon-test.nc"
@@ -467,9 +457,10 @@ def test_reload(qtbot, test_dir, tmp_path) -> None:
ds_widget = DatasetWidget(psy.open_dataset(str(tmp_path / f1)))
qtbot.addWidget(ds_widget)
- qtbot.mouseClick(ds_widget.variable_buttons['t2m'], Qt.LeftButton)
+ qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton)
assert ds_widget.ds_tree.topLevelItemCount() == 1
+ assert ds_widget.ds is not None
assert ds_widget.ds["t2m"].ndim == 4
# now copy the icon file to the same destination and reload everything
@@ -477,5 +468,6 @@ def test_reload(qtbot, test_dir, tmp_path) -> None:
ds_widget.reload()
assert ds_widget.ds_tree.topLevelItemCount() == 1
+ assert ds_widget.ds is not None
assert ds_widget.ds["t2m"].ndim == 3
assert len(psy.gcp(True)) == 1
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..2fd78de
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,24 @@
+; SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+;
+; SPDX-License-Identifier: CC0-1.0
+
+[tox]
+
+[testenv]
+extras =
+ testsite
+
+commands =
+ ; mypy psy_view
+ isort --check psy_view
+ black --line-length 79 --check psy_view
+ blackdoc --check psy_view
+ flake8 psy_view
+ pytest -v --cov=psy_view -x
+ reuse lint
+ cffconvert --validate
+
+[pytest]
+DJANGO_SETTINGS_MODULE = testproject.settings
+python_files = tests.py test_*.py *_tests.py
+norecursedirs = .* build dist *.egg venv docs
diff --git a/versioneer.py b/versioneer.py
deleted file mode 100644
index 0e4398e..0000000
--- a/versioneer.py
+++ /dev/null
@@ -1,1847 +0,0 @@
-
-# Version: 0.18
-
-"""The Versioneer - like a rocketeer, but for versions.
-
-The Versioneer
-==============
-
-* like a rocketeer, but for versions!
-* https://github.com/warner/python-versioneer
-* Brian Warner
-* License: Public Domain
-* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy
-* [![Latest Version]
-(https://pypip.in/version/versioneer/badge.svg?style=flat)
-](https://pypi.python.org/pypi/versioneer/)
-* [![Build Status]
-(https://travis-ci.org/warner/python-versioneer.png?branch=master)
-](https://travis-ci.org/warner/python-versioneer)
-
-This is a tool for managing a recorded version number in distutils-based
-python projects. The goal is to remove the tedious and error-prone "update
-the embedded version string" step from your release process. Making a new
-release should be as easy as recording a new tag in your version-control
-system, and maybe making new tarballs.
-
-
-## Quick Install
-
-* `pip install versioneer` to somewhere to your $PATH
-* add a `[versioneer]` section to your setup.cfg (see below)
-* run `versioneer install` in your source tree, commit the results
-
-## Version Identifiers
-
-Source trees come from a variety of places:
-
-* a version-control system checkout (mostly used by developers)
-* a nightly tarball, produced by build automation
-* a snapshot tarball, produced by a web-based VCS browser, like github's
- "tarball from tag" feature
-* a release tarball, produced by "setup.py sdist", distributed through PyPI
-
-Within each source tree, the version identifier (either a string or a number,
-this tool is format-agnostic) can come from a variety of places:
-
-* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows
- about recent "tags" and an absolute revision-id
-* the name of the directory into which the tarball was unpacked
-* an expanded VCS keyword ($Id$, etc)
-* a `_version.py` created by some earlier build step
-
-For released software, the version identifier is closely related to a VCS
-tag. Some projects use tag names that include more than just the version
-string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool
-needs to strip the tag prefix to extract the version identifier. For
-unreleased software (between tags), the version identifier should provide
-enough information to help developers recreate the same tree, while also
-giving them an idea of roughly how old the tree is (after version 1.2, before
-version 1.3). Many VCS systems can report a description that captures this,
-for example `git describe --tags --dirty --always` reports things like
-"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the
-0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has
-uncommitted changes.
-
-The version identifier is used for multiple purposes:
-
-* to allow the module to self-identify its version: `myproject.__version__`
-* to choose a name and prefix for a 'setup.py sdist' tarball
-
-## Theory of Operation
-
-Versioneer works by adding a special `_version.py` file into your source
-tree, where your `__init__.py` can import it. This `_version.py` knows how to
-dynamically ask the VCS tool for version information at import time.
-
-`_version.py` also contains `$Revision$` markers, and the installation
-process marks `_version.py` to have this marker rewritten with a tag name
-during the `git archive` command. As a result, generated tarballs will
-contain enough information to get the proper version.
-
-To allow `setup.py` to compute a version too, a `versioneer.py` is added to
-the top level of your source tree, next to `setup.py` and the `setup.cfg`
-that configures it. This overrides several distutils/setuptools commands to
-compute the version when invoked, and changes `setup.py build` and `setup.py
-sdist` to replace `_version.py` with a small static file that contains just
-the generated version data.
-
-## Installation
-
-See [INSTALL.md](./INSTALL.md) for detailed installation instructions.
-
-## Version-String Flavors
-
-Code which uses Versioneer can learn about its version string at runtime by
-importing `_version` from your main `__init__.py` file and running the
-`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can
-import the top-level `versioneer.py` and run `get_versions()`.
-
-Both functions return a dictionary with different flavors of version
-information:
-
-* `['version']`: A condensed version string, rendered using the selected
- style. This is the most commonly used value for the project's version
- string. The default "pep440" style yields strings like `0.11`,
- `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section
- below for alternative styles.
-
-* `['full-revisionid']`: detailed revision identifier. For Git, this is the
- full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac".
-
-* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the
- commit date in ISO 8601 format. This will be None if the date is not
- available.
-
-* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that
- this is only accurate if run in a VCS checkout, otherwise it is likely to
- be False or None
-
-* `['error']`: if the version string could not be computed, this will be set
- to a string describing the problem, otherwise it will be None. It may be
- useful to throw an exception in setup.py if this is set, to avoid e.g.
- creating tarballs with a version string of "unknown".
-
-Some variants are more useful than others. Including `full-revisionid` in a
-bug report should allow developers to reconstruct the exact code being tested
-(or indicate the presence of local changes that should be shared with the
-developers). `version` is suitable for display in an "about" box or a CLI
-`--version` output: it can be easily compared against release notes and lists
-of bugs fixed in various releases.
-
-The installer adds the following text to your `__init__.py` to place a basic
-version in `YOURPROJECT.__version__`:
-
- from ._version import get_versions
- __version__ = get_versions()['version']
- del get_versions
-
-## Styles
-
-The setup.cfg `style=` configuration controls how the VCS information is
-rendered into a version string.
-
-The default style, "pep440", produces a PEP440-compliant string, equal to the
-un-prefixed tag name for actual releases, and containing an additional "local
-version" section with more detail for in-between builds. For Git, this is
-TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags
---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the
-tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and
-that this commit is two revisions ("+2") beyond the "0.11" tag. For released
-software (exactly equal to a known tag), the identifier will only contain the
-stripped tag, e.g. "0.11".
-
-Other styles are available. See [details.md](details.md) in the Versioneer
-source tree for descriptions.
-
-## Debugging
-
-Versioneer tries to avoid fatal errors: if something goes wrong, it will tend
-to return a version of "0+unknown". To investigate the problem, run `setup.py
-version`, which will run the version-lookup code in a verbose mode, and will
-display the full contents of `get_versions()` (including the `error` string,
-which may help identify what went wrong).
-
-## Known Limitations
-
-Some situations are known to cause problems for Versioneer. This details the
-most significant ones. More can be found on Github
-[issues page](https://github.com/warner/python-versioneer/issues).
-
-### Subprojects
-
-Versioneer has limited support for source trees in which `setup.py` is not in
-the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are
-two common reasons why `setup.py` might not be in the root:
-
-* Source trees which contain multiple subprojects, such as
- [Buildbot](https://github.com/buildbot/buildbot), which contains both
- "master" and "slave" subprojects, each with their own `setup.py`,
- `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI
- distributions (and upload multiple independently-installable tarballs).
-* Source trees whose main purpose is to contain a C library, but which also
- provide bindings to Python (and perhaps other langauges) in subdirectories.
-
-Versioneer will look for `.git` in parent directories, and most operations
-should get the right version string. However `pip` and `setuptools` have bugs
-and implementation details which frequently cause `pip install .` from a
-subproject directory to fail to find a correct version string (so it usually
-defaults to `0+unknown`).
-
-`pip install --editable .` should work correctly. `setup.py install` might
-work too.
-
-Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in
-some later version.
-
-[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking
-this issue. The discussion in
-[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the
-issue from the Versioneer side in more detail.
-[pip PR#3176](https://github.com/pypa/pip/pull/3176) and
-[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve
-pip to let Versioneer work correctly.
-
-Versioneer-0.16 and earlier only looked for a `.git` directory next to the
-`setup.cfg`, so subprojects were completely unsupported with those releases.
-
-### Editable installs with setuptools <= 18.5
-
-`setup.py develop` and `pip install --editable .` allow you to install a
-project into a virtualenv once, then continue editing the source code (and
-test) without re-installing after every change.
-
-"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a
-convenient way to specify executable scripts that should be installed along
-with the python package.
-
-These both work as expected when using modern setuptools. When using
-setuptools-18.5 or earlier, however, certain operations will cause
-`pkg_resources.DistributionNotFound` errors when running the entrypoint
-script, which must be resolved by re-installing the package. This happens
-when the install happens with one version, then the egg_info data is
-regenerated while a different version is checked out. Many setup.py commands
-cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into
-a different virtualenv), so this can be surprising.
-
-[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes
-this one, but upgrading to a newer version of setuptools should probably
-resolve it.
-
-### Unicode version strings
-
-While Versioneer works (and is continually tested) with both Python 2 and
-Python 3, it is not entirely consistent with bytes-vs-unicode distinctions.
-Newer releases probably generate unicode version strings on py2. It's not
-clear that this is wrong, but it may be surprising for applications when then
-write these strings to a network connection or include them in bytes-oriented
-APIs like cryptographic checksums.
-
-[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates
-this question.
-
-
-## Updating Versioneer
-
-To upgrade your project to a new release of Versioneer, do the following:
-
-* install the new Versioneer (`pip install -U versioneer` or equivalent)
-* edit `setup.cfg`, if necessary, to include any new configuration settings
- indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details.
-* re-run `versioneer install` in your source tree, to replace
- `SRC/_version.py`
-* commit any changed files
-
-## Future Directions
-
-This tool is designed to make it easily extended to other version-control
-systems: all VCS-specific components are in separate directories like
-src/git/ . The top-level `versioneer.py` script is assembled from these
-components by running make-versioneer.py . In the future, make-versioneer.py
-will take a VCS name as an argument, and will construct a version of
-`versioneer.py` that is specific to the given VCS. It might also take the
-configuration arguments that are currently provided manually during
-installation by editing setup.py . Alternatively, it might go the other
-direction and include code from all supported VCS systems, reducing the
-number of intermediate scripts.
-
-
-## License
-
-To make Versioneer easier to embed, all its code is dedicated to the public
-domain. The `_version.py` that it creates is also in the public domain.
-Specifically, both are released under the Creative Commons "Public Domain
-Dedication" license (CC0-1.0), as described in
-https://creativecommons.org/publicdomain/zero/1.0/ .
-
-"""
-
-# Disclaimer
-# ----------
-#
-# Copyright (C) 2021 Helmholtz-Zentrum Hereon
-# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht
-#
-# This file is part of psy-view and is released under the GNU LGPL-3.O license.
-# See COPYING and COPYING.LESSER in the root of the repository for full
-# licensing details.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3.0 as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LGPL-3.0 license for more details.
-#
-# You should have received a copy of the GNU LGPL-3.0 license
-# along with this program. If not, see .
-#
-# This file is originally released into the public domain. Generated by
-# versioneer-0.18 (https://github.com/warner/python-versioneer)
-
-from __future__ import print_function
-try:
- import configparser
-except ImportError:
- import ConfigParser as configparser
-import errno
-import json
-import os
-import re
-import subprocess
-import sys
-
-
-class VersioneerConfig:
- """Container for Versioneer configuration parameters."""
-
-
-def get_root():
- """Get the project root directory.
-
- We require that all commands are run from the project root, i.e. the
- directory that contains setup.py, setup.cfg, and versioneer.py .
- """
- root = os.path.realpath(os.path.abspath(os.getcwd()))
- setup_py = os.path.join(root, "setup.py")
- versioneer_py = os.path.join(root, "versioneer.py")
- if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
- # allow 'python path/to/setup.py COMMAND'
- root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))
- setup_py = os.path.join(root, "setup.py")
- versioneer_py = os.path.join(root, "versioneer.py")
- if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
- err = ("Versioneer was unable to run the project root directory. "
- "Versioneer requires setup.py to be executed from "
- "its immediate directory (like 'python setup.py COMMAND'), "
- "or in a way that lets it use sys.argv[0] to find the root "
- "(like 'python path/to/setup.py COMMAND').")
- raise VersioneerBadRootError(err)
- try:
- # Certain runtime workflows (setup.py install/develop in a setuptools
- # tree) execute all dependencies in a single python process, so
- # "versioneer" may be imported multiple times, and python's shared
- # module-import table will cache the first one. So we can't use
- # os.path.dirname(__file__), as that will find whichever
- # versioneer.py was first imported, even in later projects.
- me = os.path.realpath(os.path.abspath(__file__))
- me_dir = os.path.normcase(os.path.splitext(me)[0])
- vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0])
- if me_dir != vsr_dir:
- print("Warning: build in %s is using versioneer.py from %s"
- % (os.path.dirname(me), versioneer_py))
- except NameError:
- pass
- return root
-
-
-def get_config_from_root(root):
- """Read the project setup.cfg file to determine Versioneer config."""
- # This might raise EnvironmentError (if setup.cfg is missing), or
- # configparser.NoSectionError (if it lacks a [versioneer] section), or
- # configparser.NoOptionError (if it lacks "VCS="). See the docstring at
- # the top of versioneer.py for instructions on writing your setup.cfg .
- setup_cfg = os.path.join(root, "setup.cfg")
- parser = configparser.SafeConfigParser()
- with open(setup_cfg, "r") as f:
- parser.readfp(f)
- VCS = parser.get("versioneer", "VCS") # mandatory
-
- def get(parser, name):
- if parser.has_option("versioneer", name):
- return parser.get("versioneer", name)
- return None
- cfg = VersioneerConfig()
- cfg.VCS = VCS
- cfg.style = get(parser, "style") or ""
- cfg.versionfile_source = get(parser, "versionfile_source")
- cfg.versionfile_build = get(parser, "versionfile_build")
- cfg.tag_prefix = get(parser, "tag_prefix")
- if cfg.tag_prefix in ("''", '""'):
- cfg.tag_prefix = ""
- cfg.parentdir_prefix = get(parser, "parentdir_prefix")
- cfg.verbose = get(parser, "verbose")
- return cfg
-
-
-class NotThisMethod(Exception):
- """Exception raised if a method is not valid for the current scenario."""
-
-
-# these dictionaries contain VCS-specific tools
-LONG_VERSION_PY = {}
-HANDLERS = {}
-
-
-def register_vcs_handler(vcs, method): # decorator
- """Decorator to mark a method as the handler for a particular VCS."""
- def decorate(f):
- """Store f in HANDLERS[vcs][method]."""
- if vcs not in HANDLERS:
- HANDLERS[vcs] = {}
- HANDLERS[vcs][method] = f
- return f
- return decorate
-
-
-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
- env=None):
- """Call the given command(s)."""
- assert isinstance(commands, list)
- p = None
- for c in commands:
- try:
- dispcmd = str([c] + args)
- # remember shell=False, so use git.cmd on windows, not just git
- p = subprocess.Popen([c] + args, cwd=cwd, env=env,
- stdout=subprocess.PIPE,
- stderr=(subprocess.PIPE if hide_stderr
- else None))
- break
- except EnvironmentError:
- e = sys.exc_info()[1]
- if e.errno == errno.ENOENT:
- continue
- if verbose:
- print("unable to run %s" % dispcmd)
- print(e)
- return None, None
- else:
- if verbose:
- print("unable to find command, tried %s" % (commands,))
- return None, None
- stdout = p.communicate()[0].strip()
- if sys.version_info[0] >= 3:
- stdout = stdout.decode()
- if p.returncode != 0:
- if verbose:
- print("unable to run %s (error)" % dispcmd)
- print("stdout was %s" % stdout)
- return None, p.returncode
- return stdout, p.returncode
-
-
-LONG_VERSION_PY['git'] = '''
-# This file helps to compute a version number in source trees obtained from
-# git-archive tarball (such as those provided by githubs download-from-tag
-# feature). Distribution tarballs (built by setup.py sdist) and build
-# directories (produced by setup.py build) will contain a much shorter file
-# that just contains the computed version number.
-
-# This file is released into the public domain. Generated by
-# versioneer-0.18 (https://github.com/warner/python-versioneer)
-
-"""Git implementation of _version.py."""
-
-import errno
-import os
-import re
-import subprocess
-import sys
-
-
-def get_keywords():
- """Get the keywords needed to look up the version information."""
- # these strings will be replaced by git during git-archive.
- # setup.py/versioneer.py will grep for the variable names, so they must
- # each be defined on a line of their own. _version.py will just call
- # get_keywords().
- git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
- git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
- git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s"
- keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
- return keywords
-
-
-class VersioneerConfig:
- """Container for Versioneer configuration parameters."""
-
-
-def get_config():
- """Create, populate and return the VersioneerConfig() object."""
- # these strings are filled in when 'setup.py versioneer' creates
- # _version.py
- cfg = VersioneerConfig()
- cfg.VCS = "git"
- cfg.style = "%(STYLE)s"
- cfg.tag_prefix = "%(TAG_PREFIX)s"
- cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s"
- cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s"
- cfg.verbose = False
- return cfg
-
-
-class NotThisMethod(Exception):
- """Exception raised if a method is not valid for the current scenario."""
-
-
-LONG_VERSION_PY = {}
-HANDLERS = {}
-
-
-def register_vcs_handler(vcs, method): # decorator
- """Decorator to mark a method as the handler for a particular VCS."""
- def decorate(f):
- """Store f in HANDLERS[vcs][method]."""
- if vcs not in HANDLERS:
- HANDLERS[vcs] = {}
- HANDLERS[vcs][method] = f
- return f
- return decorate
-
-
-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
- env=None):
- """Call the given command(s)."""
- assert isinstance(commands, list)
- p = None
- for c in commands:
- try:
- dispcmd = str([c] + args)
- # remember shell=False, so use git.cmd on windows, not just git
- p = subprocess.Popen([c] + args, cwd=cwd, env=env,
- stdout=subprocess.PIPE,
- stderr=(subprocess.PIPE if hide_stderr
- else None))
- break
- except EnvironmentError:
- e = sys.exc_info()[1]
- if e.errno == errno.ENOENT:
- continue
- if verbose:
- print("unable to run %%s" %% dispcmd)
- print(e)
- return None, None
- else:
- if verbose:
- print("unable to find command, tried %%s" %% (commands,))
- return None, None
- stdout = p.communicate()[0].strip()
- if sys.version_info[0] >= 3:
- stdout = stdout.decode()
- if p.returncode != 0:
- if verbose:
- print("unable to run %%s (error)" %% dispcmd)
- print("stdout was %%s" %% stdout)
- return None, p.returncode
- return stdout, p.returncode
-
-
-def versions_from_parentdir(parentdir_prefix, root, verbose):
- """Try to determine the version from the parent directory name.
-
- Source tarballs conventionally unpack into a directory that includes both
- the project name and a version string. We will also support searching up
- two directory levels for an appropriately named parent directory
- """
- rootdirs = []
-
- for i in range(3):
- dirname = os.path.basename(root)
- if dirname.startswith(parentdir_prefix):
- return {"version": dirname[len(parentdir_prefix):],
- "full-revisionid": None,
- "dirty": False, "error": None, "date": None}
- else:
- rootdirs.append(root)
- root = os.path.dirname(root) # up a level
-
- if verbose:
- print("Tried directories %%s but none started with prefix %%s" %%
- (str(rootdirs), parentdir_prefix))
- raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
-
-
-@register_vcs_handler("git", "get_keywords")
-def git_get_keywords(versionfile_abs):
- """Extract version information from the given file."""
- # the code embedded in _version.py can just fetch the value of these
- # keywords. When used from setup.py, we don't want to import _version.py,
- # so we do it with a regexp instead. This function is not used from
- # _version.py.
- keywords = {}
- try:
- f = open(versionfile_abs, "r")
- for line in f.readlines():
- if line.strip().startswith("git_refnames ="):
- mo = re.search(r'=\s*"(.*)"', line)
- if mo:
- keywords["refnames"] = mo.group(1)
- if line.strip().startswith("git_full ="):
- mo = re.search(r'=\s*"(.*)"', line)
- if mo:
- keywords["full"] = mo.group(1)
- if line.strip().startswith("git_date ="):
- mo = re.search(r'=\s*"(.*)"', line)
- if mo:
- keywords["date"] = mo.group(1)
- f.close()
- except EnvironmentError:
- pass
- return keywords
-
-
-@register_vcs_handler("git", "keywords")
-def git_versions_from_keywords(keywords, tag_prefix, verbose):
- """Get version information from git keywords."""
- if not keywords:
- raise NotThisMethod("no keywords at all, weird")
- date = keywords.get("date")
- if date is not None:
- # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant
- # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601
- # -like" string, which we must then edit to make compliant), because
- # it's been around since git-1.5.3, and it's too difficult to
- # discover which version we're using, or to work around using an
- # older one.
- date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
- refnames = keywords["refnames"].strip()
- if refnames.startswith("$Format"):
- if verbose:
- print("keywords are unexpanded, not using")
- raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
- refs = set([r.strip() for r in refnames.strip("()").split(",")])
- # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
- # just "foo-1.0". If we see a "tag: " prefix, prefer those.
- TAG = "tag: "
- tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
- if not tags:
- # Either we're using git < 1.8.3, or there really are no tags. We use
- # a heuristic: assume all version tags have a digit. The old git %%d
- # expansion behaves like git log --decorate=short and strips out the
- # refs/heads/ and refs/tags/ prefixes that would let us distinguish
- # between branches and tags. By ignoring refnames without digits, we
- # filter out many common branch names like "release" and
- # "stabilization", as well as "HEAD" and "master".
- tags = set([r for r in refs if re.search(r'\d', r)])
- if verbose:
- print("discarding '%%s', no digits" %% ",".join(refs - tags))
- if verbose:
- print("likely tags: %%s" %% ",".join(sorted(tags)))
- for ref in sorted(tags):
- # sorting will prefer e.g. "2.0" over "2.0rc1"
- if ref.startswith(tag_prefix):
- r = ref[len(tag_prefix):]
- if verbose:
- print("picking %%s" %% r)
- return {"version": r,
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": None,
- "date": date}
- # no suitable tags, so version is "0+unknown", but full hex is still there
- if verbose:
- print("no suitable tags, using unknown + full revision id")
- return {"version": "0+unknown",
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": "no suitable tags", "date": None}
-
-
-@register_vcs_handler("git", "pieces_from_vcs")
-def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
- """Get version from 'git describe' in the root of the source tree.
-
- This only gets called if the git-archive 'subst' keywords were *not*
- expanded, and _version.py hasn't already been rewritten with a short
- version string, meaning we're inside a checked out source tree.
- """
- GITS = ["git"]
- if sys.platform == "win32":
- GITS = ["git.cmd", "git.exe"]
-
- out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
- hide_stderr=True)
- if rc != 0:
- if verbose:
- print("Directory %%s not under git control" %% root)
- raise NotThisMethod("'git rev-parse --git-dir' returned error")
-
- # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
- # if there isn't one, this yields HEX[-dirty] (no NUM)
- describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
- "--always", "--long",
- "--match", "%%s*" %% tag_prefix],
- cwd=root)
- # --long was added in git-1.5.5
- if describe_out is None:
- raise NotThisMethod("'git describe' failed")
- describe_out = describe_out.strip()
- full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
- if full_out is None:
- raise NotThisMethod("'git rev-parse' failed")
- full_out = full_out.strip()
-
- pieces = {}
- pieces["long"] = full_out
- pieces["short"] = full_out[:7] # maybe improved later
- pieces["error"] = None
-
- # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
- # TAG might have hyphens.
- git_describe = describe_out
-
- # look for -dirty suffix
- dirty = git_describe.endswith("-dirty")
- pieces["dirty"] = dirty
- if dirty:
- git_describe = git_describe[:git_describe.rindex("-dirty")]
-
- # now we have TAG-NUM-gHEX or HEX
-
- if "-" in git_describe:
- # TAG-NUM-gHEX
- mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
- if not mo:
- # unparseable. Maybe git-describe is misbehaving?
- pieces["error"] = ("unable to parse git-describe output: '%%s'"
- %% describe_out)
- return pieces
-
- # tag
- full_tag = mo.group(1)
- if not full_tag.startswith(tag_prefix):
- if verbose:
- fmt = "tag '%%s' doesn't start with prefix '%%s'"
- print(fmt %% (full_tag, tag_prefix))
- pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'"
- %% (full_tag, tag_prefix))
- return pieces
- pieces["closest-tag"] = full_tag[len(tag_prefix):]
-
- # distance: number of commits since tag
- pieces["distance"] = int(mo.group(2))
-
- # commit: short hex revision ID
- pieces["short"] = mo.group(3)
-
- else:
- # HEX: no tags
- pieces["closest-tag"] = None
- count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
- cwd=root)
- pieces["distance"] = int(count_out) # total number of commits
-
- # commit date: see ISO-8601 comment in git_versions_from_keywords()
- date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"],
- cwd=root)[0].strip()
- pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-
- return pieces
-
-
-def plus_or_dot(pieces):
- """Return a + if we don't already have one, else return a ."""
- if "+" in pieces.get("closest-tag", ""):
- return "."
- return "+"
-
-
-def render_pep440(pieces):
- """Build up version string, with post-release "local version identifier".
-
- Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
- get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
-
- Exceptions:
- 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"] or pieces["dirty"]:
- rendered += plus_or_dot(pieces)
- rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
- if pieces["dirty"]:
- rendered += ".dirty"
- else:
- # exception #1
- rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"],
- pieces["short"])
- if pieces["dirty"]:
- rendered += ".dirty"
- return rendered
-
-
-def render_pep440_pre(pieces):
- """TAG[.post.devDISTANCE] -- No -dirty.
-
- Exceptions:
- 1: no tags. 0.post.devDISTANCE
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"]:
- rendered += ".post.dev%%d" %% pieces["distance"]
- else:
- # exception #1
- rendered = "0.post.dev%%d" %% pieces["distance"]
- return rendered
-
-
-def render_pep440_post(pieces):
- """TAG[.postDISTANCE[.dev0]+gHEX] .
-
- The ".dev0" means dirty. Note that .dev0 sorts backwards
- (a dirty tree will appear "older" than the corresponding clean one),
- but you shouldn't be releasing software with -dirty anyways.
-
- Exceptions:
- 1: no tags. 0.postDISTANCE[.dev0]
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"] or pieces["dirty"]:
- rendered += ".post%%d" %% pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- rendered += plus_or_dot(pieces)
- rendered += "g%%s" %% pieces["short"]
- else:
- # exception #1
- rendered = "0.post%%d" %% pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- rendered += "+g%%s" %% pieces["short"]
- return rendered
-
-
-def render_pep440_old(pieces):
- """TAG[.postDISTANCE[.dev0]] .
-
- The ".dev0" means dirty.
-
- Eexceptions:
- 1: no tags. 0.postDISTANCE[.dev0]
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"] or pieces["dirty"]:
- rendered += ".post%%d" %% pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- else:
- # exception #1
- rendered = "0.post%%d" %% pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- return rendered
-
-
-def render_git_describe(pieces):
- """TAG[-DISTANCE-gHEX][-dirty].
-
- Like 'git describe --tags --dirty --always'.
-
- Exceptions:
- 1: no tags. HEX[-dirty] (note: no 'g' prefix)
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"]:
- rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
- else:
- # exception #1
- rendered = pieces["short"]
- if pieces["dirty"]:
- rendered += "-dirty"
- return rendered
-
-
-def render_git_describe_long(pieces):
- """TAG-DISTANCE-gHEX[-dirty].
-
- Like 'git describe --tags --dirty --always -long'.
- The distance/hash is unconditional.
-
- Exceptions:
- 1: no tags. HEX[-dirty] (note: no 'g' prefix)
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
- else:
- # exception #1
- rendered = pieces["short"]
- if pieces["dirty"]:
- rendered += "-dirty"
- return rendered
-
-
-def render(pieces, style):
- """Render the given version pieces into the requested style."""
- if pieces["error"]:
- return {"version": "unknown",
- "full-revisionid": pieces.get("long"),
- "dirty": None,
- "error": pieces["error"],
- "date": None}
-
- if not style or style == "default":
- style = "pep440" # the default
-
- if style == "pep440":
- rendered = render_pep440(pieces)
- elif style == "pep440-pre":
- rendered = render_pep440_pre(pieces)
- elif style == "pep440-post":
- rendered = render_pep440_post(pieces)
- elif style == "pep440-old":
- rendered = render_pep440_old(pieces)
- elif style == "git-describe":
- rendered = render_git_describe(pieces)
- elif style == "git-describe-long":
- rendered = render_git_describe_long(pieces)
- else:
- raise ValueError("unknown style '%%s'" %% style)
-
- return {"version": rendered, "full-revisionid": pieces["long"],
- "dirty": pieces["dirty"], "error": None,
- "date": pieces.get("date")}
-
-
-def get_versions():
- """Get version information or return default if unable to do so."""
- # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
- # __file__, we can work backwards from there to the root. Some
- # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
- # case we can only use expanded keywords.
-
- cfg = get_config()
- verbose = cfg.verbose
-
- try:
- return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
- verbose)
- except NotThisMethod:
- pass
-
- try:
- root = os.path.realpath(__file__)
- # versionfile_source is the relative path from the top of the source
- # tree (where the .git directory might live) to this file. Invert
- # this to find the root from __file__.
- for i in cfg.versionfile_source.split('/'):
- root = os.path.dirname(root)
- except NameError:
- return {"version": "0+unknown", "full-revisionid": None,
- "dirty": None,
- "error": "unable to find root of source tree",
- "date": None}
-
- try:
- pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
- return render(pieces, cfg.style)
- except NotThisMethod:
- pass
-
- try:
- if cfg.parentdir_prefix:
- return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
- except NotThisMethod:
- pass
-
- return {"version": "0+unknown", "full-revisionid": None,
- "dirty": None,
- "error": "unable to compute version", "date": None}
-'''
-
-
-@register_vcs_handler("git", "get_keywords")
-def git_get_keywords(versionfile_abs):
- """Extract version information from the given file."""
- # the code embedded in _version.py can just fetch the value of these
- # keywords. When used from setup.py, we don't want to import _version.py,
- # so we do it with a regexp instead. This function is not used from
- # _version.py.
- keywords = {}
- try:
- f = open(versionfile_abs, "r")
- for line in f.readlines():
- if line.strip().startswith("git_refnames ="):
- mo = re.search(r'=\s*"(.*)"', line)
- if mo:
- keywords["refnames"] = mo.group(1)
- if line.strip().startswith("git_full ="):
- mo = re.search(r'=\s*"(.*)"', line)
- if mo:
- keywords["full"] = mo.group(1)
- if line.strip().startswith("git_date ="):
- mo = re.search(r'=\s*"(.*)"', line)
- if mo:
- keywords["date"] = mo.group(1)
- f.close()
- except EnvironmentError:
- pass
- return keywords
-
-
-@register_vcs_handler("git", "keywords")
-def git_versions_from_keywords(keywords, tag_prefix, verbose):
- """Get version information from git keywords."""
- if not keywords:
- raise NotThisMethod("no keywords at all, weird")
- date = keywords.get("date")
- if date is not None:
- # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
- # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
- # -like" string, which we must then edit to make compliant), because
- # it's been around since git-1.5.3, and it's too difficult to
- # discover which version we're using, or to work around using an
- # older one.
- date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
- refnames = keywords["refnames"].strip()
- if refnames.startswith("$Format"):
- if verbose:
- print("keywords are unexpanded, not using")
- raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
- refs = set([r.strip() for r in refnames.strip("()").split(",")])
- # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
- # just "foo-1.0". If we see a "tag: " prefix, prefer those.
- TAG = "tag: "
- tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
- if not tags:
- # Either we're using git < 1.8.3, or there really are no tags. We use
- # a heuristic: assume all version tags have a digit. The old git %d
- # expansion behaves like git log --decorate=short and strips out the
- # refs/heads/ and refs/tags/ prefixes that would let us distinguish
- # between branches and tags. By ignoring refnames without digits, we
- # filter out many common branch names like "release" and
- # "stabilization", as well as "HEAD" and "master".
- tags = set([r for r in refs if re.search(r'\d', r)])
- if verbose:
- print("discarding '%s', no digits" % ",".join(refs - tags))
- if verbose:
- print("likely tags: %s" % ",".join(sorted(tags)))
- for ref in sorted(tags):
- # sorting will prefer e.g. "2.0" over "2.0rc1"
- if ref.startswith(tag_prefix):
- r = ref[len(tag_prefix):]
- if verbose:
- print("picking %s" % r)
- return {"version": r,
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": None,
- "date": date}
- # no suitable tags, so version is "0+unknown", but full hex is still there
- if verbose:
- print("no suitable tags, using unknown + full revision id")
- return {"version": "0+unknown",
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": "no suitable tags", "date": None}
-
-
-@register_vcs_handler("git", "pieces_from_vcs")
-def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
- """Get version from 'git describe' in the root of the source tree.
-
- This only gets called if the git-archive 'subst' keywords were *not*
- expanded, and _version.py hasn't already been rewritten with a short
- version string, meaning we're inside a checked out source tree.
- """
- GITS = ["git"]
- if sys.platform == "win32":
- GITS = ["git.cmd", "git.exe"]
-
- out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
- hide_stderr=True)
- if rc != 0:
- if verbose:
- print("Directory %s not under git control" % root)
- raise NotThisMethod("'git rev-parse --git-dir' returned error")
-
- # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
- # if there isn't one, this yields HEX[-dirty] (no NUM)
- describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
- "--always", "--long",
- "--match", "%s*" % tag_prefix],
- cwd=root)
- # --long was added in git-1.5.5
- if describe_out is None:
- raise NotThisMethod("'git describe' failed")
- describe_out = describe_out.strip()
- full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
- if full_out is None:
- raise NotThisMethod("'git rev-parse' failed")
- full_out = full_out.strip()
-
- pieces = {}
- pieces["long"] = full_out
- pieces["short"] = full_out[:7] # maybe improved later
- pieces["error"] = None
-
- # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
- # TAG might have hyphens.
- git_describe = describe_out
-
- # look for -dirty suffix
- dirty = git_describe.endswith("-dirty")
- pieces["dirty"] = dirty
- if dirty:
- git_describe = git_describe[:git_describe.rindex("-dirty")]
-
- # now we have TAG-NUM-gHEX or HEX
-
- if "-" in git_describe:
- # TAG-NUM-gHEX
- mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
- if not mo:
- # unparseable. Maybe git-describe is misbehaving?
- pieces["error"] = ("unable to parse git-describe output: '%s'"
- % describe_out)
- return pieces
-
- # tag
- full_tag = mo.group(1)
- if not full_tag.startswith(tag_prefix):
- if verbose:
- fmt = "tag '%s' doesn't start with prefix '%s'"
- print(fmt % (full_tag, tag_prefix))
- pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
- % (full_tag, tag_prefix))
- return pieces
- pieces["closest-tag"] = full_tag[len(tag_prefix):]
-
- # distance: number of commits since tag
- pieces["distance"] = int(mo.group(2))
-
- # commit: short hex revision ID
- pieces["short"] = mo.group(3)
-
- else:
- # HEX: no tags
- pieces["closest-tag"] = None
- count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
- cwd=root)
- pieces["distance"] = int(count_out) # total number of commits
-
- # commit date: see ISO-8601 comment in git_versions_from_keywords()
- date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
- cwd=root)[0].strip()
- pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-
- return pieces
-
-
-def do_vcs_install(manifest_in, versionfile_source, ipy):
- """Git-specific installation logic for Versioneer.
-
- For Git, this means creating/changing .gitattributes to mark _version.py
- for export-subst keyword substitution.
- """
- GITS = ["git"]
- if sys.platform == "win32":
- GITS = ["git.cmd", "git.exe"]
- files = [manifest_in, versionfile_source]
- if ipy:
- files.append(ipy)
- try:
- me = __file__
- if me.endswith(".pyc") or me.endswith(".pyo"):
- me = os.path.splitext(me)[0] + ".py"
- versioneer_file = os.path.relpath(me)
- except NameError:
- versioneer_file = "versioneer.py"
- files.append(versioneer_file)
- present = False
- try:
- f = open(".gitattributes", "r")
- for line in f.readlines():
- if line.strip().startswith(versionfile_source):
- if "export-subst" in line.strip().split()[1:]:
- present = True
- f.close()
- except EnvironmentError:
- pass
- if not present:
- f = open(".gitattributes", "a+")
- f.write("%s export-subst\n" % versionfile_source)
- f.close()
- files.append(".gitattributes")
- run_command(GITS, ["add", "--"] + files)
-
-
-def versions_from_parentdir(parentdir_prefix, root, verbose):
- """Try to determine the version from the parent directory name.
-
- Source tarballs conventionally unpack into a directory that includes both
- the project name and a version string. We will also support searching up
- two directory levels for an appropriately named parent directory
- """
- rootdirs = []
-
- for i in range(3):
- dirname = os.path.basename(root)
- if dirname.startswith(parentdir_prefix):
- return {"version": dirname[len(parentdir_prefix):],
- "full-revisionid": None,
- "dirty": False, "error": None, "date": None}
- else:
- rootdirs.append(root)
- root = os.path.dirname(root) # up a level
-
- if verbose:
- print("Tried directories %s but none started with prefix %s" %
- (str(rootdirs), parentdir_prefix))
- raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
-
-
-SHORT_VERSION_PY = """
-# This file was generated by 'versioneer.py' (0.18) from
-# revision-control system data, or from the parent directory name of an
-# unpacked source archive. Distribution tarballs contain a pre-generated copy
-# of this file.
-
-import json
-
-version_json = '''
-%s
-''' # END VERSION_JSON
-
-
-def get_versions():
- return json.loads(version_json)
-"""
-
-
-def versions_from_file(filename):
- """Try to determine the version from _version.py if present."""
- try:
- with open(filename) as f:
- contents = f.read()
- except EnvironmentError:
- raise NotThisMethod("unable to read _version.py")
- mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON",
- contents, re.M | re.S)
- if not mo:
- mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON",
- contents, re.M | re.S)
- if not mo:
- raise NotThisMethod("no version_json in _version.py")
- return json.loads(mo.group(1))
-
-
-def write_to_version_file(filename, versions):
- """Write the given version number to the given _version.py file."""
- os.unlink(filename)
- contents = json.dumps(versions, sort_keys=True,
- indent=1, separators=(",", ": "))
- with open(filename, "w") as f:
- f.write(SHORT_VERSION_PY % contents)
-
- print("set %s to '%s'" % (filename, versions["version"]))
-
-
-def plus_or_dot(pieces):
- """Return a + if we don't already have one, else return a ."""
- if "+" in pieces.get("closest-tag", ""):
- return "."
- return "+"
-
-
-def render_pep440(pieces):
- """Build up version string, with post-release "local version identifier".
-
- Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
- get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
-
- Exceptions:
- 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"] or pieces["dirty"]:
- rendered += plus_or_dot(pieces)
- rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
- if pieces["dirty"]:
- rendered += ".dirty"
- else:
- # exception #1
- rendered = "0+untagged.%d.g%s" % (pieces["distance"],
- pieces["short"])
- if pieces["dirty"]:
- rendered += ".dirty"
- return rendered
-
-
-def render_pep440_pre(pieces):
- """TAG[.post.devDISTANCE] -- No -dirty.
-
- Exceptions:
- 1: no tags. 0.post.devDISTANCE
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"]:
- rendered += ".post.dev%d" % pieces["distance"]
- else:
- # exception #1
- rendered = "0.post.dev%d" % pieces["distance"]
- return rendered
-
-
-def render_pep440_post(pieces):
- """TAG[.postDISTANCE[.dev0]+gHEX] .
-
- The ".dev0" means dirty. Note that .dev0 sorts backwards
- (a dirty tree will appear "older" than the corresponding clean one),
- but you shouldn't be releasing software with -dirty anyways.
-
- Exceptions:
- 1: no tags. 0.postDISTANCE[.dev0]
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"] or pieces["dirty"]:
- rendered += ".post%d" % pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- rendered += plus_or_dot(pieces)
- rendered += "g%s" % pieces["short"]
- else:
- # exception #1
- rendered = "0.post%d" % pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- rendered += "+g%s" % pieces["short"]
- return rendered
-
-
-def render_pep440_old(pieces):
- """TAG[.postDISTANCE[.dev0]] .
-
- The ".dev0" means dirty.
-
- Eexceptions:
- 1: no tags. 0.postDISTANCE[.dev0]
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"] or pieces["dirty"]:
- rendered += ".post%d" % pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- else:
- # exception #1
- rendered = "0.post%d" % pieces["distance"]
- if pieces["dirty"]:
- rendered += ".dev0"
- return rendered
-
-
-def render_git_describe(pieces):
- """TAG[-DISTANCE-gHEX][-dirty].
-
- Like 'git describe --tags --dirty --always'.
-
- Exceptions:
- 1: no tags. HEX[-dirty] (note: no 'g' prefix)
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- if pieces["distance"]:
- rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
- else:
- # exception #1
- rendered = pieces["short"]
- if pieces["dirty"]:
- rendered += "-dirty"
- return rendered
-
-
-def render_git_describe_long(pieces):
- """TAG-DISTANCE-gHEX[-dirty].
-
- Like 'git describe --tags --dirty --always -long'.
- The distance/hash is unconditional.
-
- Exceptions:
- 1: no tags. HEX[-dirty] (note: no 'g' prefix)
- """
- if pieces["closest-tag"]:
- rendered = pieces["closest-tag"]
- rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
- else:
- # exception #1
- rendered = pieces["short"]
- if pieces["dirty"]:
- rendered += "-dirty"
- return rendered
-
-
-def render(pieces, style):
- """Render the given version pieces into the requested style."""
- if pieces["error"]:
- return {"version": "unknown",
- "full-revisionid": pieces.get("long"),
- "dirty": None,
- "error": pieces["error"],
- "date": None}
-
- if not style or style == "default":
- style = "pep440" # the default
-
- if style == "pep440":
- rendered = render_pep440(pieces)
- elif style == "pep440-pre":
- rendered = render_pep440_pre(pieces)
- elif style == "pep440-post":
- rendered = render_pep440_post(pieces)
- elif style == "pep440-old":
- rendered = render_pep440_old(pieces)
- elif style == "git-describe":
- rendered = render_git_describe(pieces)
- elif style == "git-describe-long":
- rendered = render_git_describe_long(pieces)
- else:
- raise ValueError("unknown style '%s'" % style)
-
- return {"version": rendered, "full-revisionid": pieces["long"],
- "dirty": pieces["dirty"], "error": None,
- "date": pieces.get("date")}
-
-
-class VersioneerBadRootError(Exception):
- """The project root directory is unknown or missing key files."""
-
-
-def get_versions(verbose=False):
- """Get the project version from whatever source is available.
-
- Returns dict with two keys: 'version' and 'full'.
- """
- if "versioneer" in sys.modules:
- # see the discussion in cmdclass.py:get_cmdclass()
- del sys.modules["versioneer"]
-
- root = get_root()
- cfg = get_config_from_root(root)
-
- assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg"
- handlers = HANDLERS.get(cfg.VCS)
- assert handlers, "unrecognized VCS '%s'" % cfg.VCS
- verbose = verbose or cfg.verbose
- assert cfg.versionfile_source is not None, \
- "please set versioneer.versionfile_source"
- assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix"
-
- versionfile_abs = os.path.join(root, cfg.versionfile_source)
-
- # extract version from first of: _version.py, VCS command (e.g. 'git
- # describe'), parentdir. This is meant to work for developers using a
- # source checkout, for users of a tarball created by 'setup.py sdist',
- # and for users of a tarball/zipball created by 'git archive' or github's
- # download-from-tag feature or the equivalent in other VCSes.
-
- get_keywords_f = handlers.get("get_keywords")
- from_keywords_f = handlers.get("keywords")
- if get_keywords_f and from_keywords_f:
- try:
- keywords = get_keywords_f(versionfile_abs)
- ver = from_keywords_f(keywords, cfg.tag_prefix, verbose)
- if verbose:
- print("got version from expanded keyword %s" % ver)
- return ver
- except NotThisMethod:
- pass
-
- try:
- ver = versions_from_file(versionfile_abs)
- if verbose:
- print("got version from file %s %s" % (versionfile_abs, ver))
- return ver
- except NotThisMethod:
- pass
-
- from_vcs_f = handlers.get("pieces_from_vcs")
- if from_vcs_f:
- try:
- pieces = from_vcs_f(cfg.tag_prefix, root, verbose)
- ver = render(pieces, cfg.style)
- if verbose:
- print("got version from VCS %s" % ver)
- return ver
- except NotThisMethod:
- pass
-
- try:
- if cfg.parentdir_prefix:
- ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
- if verbose:
- print("got version from parentdir %s" % ver)
- return ver
- except NotThisMethod:
- pass
-
- if verbose:
- print("unable to compute version")
-
- return {"version": "0+unknown", "full-revisionid": None,
- "dirty": None, "error": "unable to compute version",
- "date": None}
-
-
-def get_version():
- """Get the short version string for this project."""
- return get_versions()["version"]
-
-
-def get_cmdclass(cmdclass=None):
- """Get the custom setuptools/distutils subclasses used by Versioneer."""
- if "versioneer" in sys.modules:
- del sys.modules["versioneer"]
- # this fixes the "python setup.py develop" case (also 'install' and
- # 'easy_install .'), in which subdependencies of the main project are
- # built (using setup.py bdist_egg) in the same python process. Assume
- # a main project A and a dependency B, which use different versions
- # of Versioneer. A's setup.py imports A's Versioneer, leaving it in
- # sys.modules by the time B's setup.py is executed, causing B to run
- # with the wrong versioneer. Setuptools wraps the sub-dep builds in a
- # sandbox that restores sys.modules to it's pre-build state, so the
- # parent is protected against the child's "import versioneer". By
- # removing ourselves from sys.modules here, before the child build
- # happens, we protect the child from the parent's versioneer too.
- # Also see https://github.com/warner/python-versioneer/issues/52
-
- cmds = {} if cmdclass is None else cmdclass.copy()
-
- # we add "version" to both distutils and setuptools
- from distutils.core import Command
-
- class cmd_version(Command):
- description = "report generated version string"
- user_options = []
- boolean_options = []
-
- def initialize_options(self):
- pass
-
- def finalize_options(self):
- pass
-
- def run(self):
- vers = get_versions(verbose=True)
- print("Version: %s" % vers["version"])
- print(" full-revisionid: %s" % vers.get("full-revisionid"))
- print(" dirty: %s" % vers.get("dirty"))
- print(" date: %s" % vers.get("date"))
- if vers["error"]:
- print(" error: %s" % vers["error"])
- cmds["version"] = cmd_version
-
- # we override "build_py" in both distutils and setuptools
- #
- # most invocation pathways end up running build_py:
- # distutils/build -> build_py
- # distutils/install -> distutils/build ->..
- # setuptools/bdist_wheel -> distutils/install ->..
- # setuptools/bdist_egg -> distutils/install_lib -> build_py
- # setuptools/install -> bdist_egg ->..
- # setuptools/develop -> ?
- # pip install:
- # copies source tree to a tempdir before running egg_info/etc
- # if .git isn't copied too, 'git describe' will fail
- # then does setup.py bdist_wheel, or sometimes setup.py install
- # setup.py egg_info -> ?
-
- # we override different "build_py" commands for both environments
- if "setuptools" in sys.modules:
- from setuptools.command.build_py import build_py as _build_py
- else:
- from distutils.command.build_py import build_py as _build_py
-
- class cmd_build_py(_build_py):
- def run(self):
- root = get_root()
- cfg = get_config_from_root(root)
- versions = get_versions()
- _build_py.run(self)
- # now locate _version.py in the new build/ directory and replace
- # it with an updated value
- if cfg.versionfile_build:
- target_versionfile = os.path.join(self.build_lib,
- cfg.versionfile_build)
- print("UPDATING %s" % target_versionfile)
- write_to_version_file(target_versionfile, versions)
- cmds["build_py"] = cmd_build_py
-
- if "cx_Freeze" in sys.modules: # cx_freeze enabled?
- from cx_Freeze.dist import build_exe as _build_exe
- # nczeczulin reports that py2exe won't like the pep440-style string
- # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g.
- # setup(console=[{
- # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION
- # "product_version": versioneer.get_version(),
- # ...
-
- class cmd_build_exe(_build_exe):
- def run(self):
- root = get_root()
- cfg = get_config_from_root(root)
- versions = get_versions()
- target_versionfile = cfg.versionfile_source
- print("UPDATING %s" % target_versionfile)
- write_to_version_file(target_versionfile, versions)
-
- _build_exe.run(self)
- os.unlink(target_versionfile)
- with open(cfg.versionfile_source, "w") as f:
- LONG = LONG_VERSION_PY[cfg.VCS]
- f.write(LONG %
- {"DOLLAR": "$",
- "STYLE": cfg.style,
- "TAG_PREFIX": cfg.tag_prefix,
- "PARENTDIR_PREFIX": cfg.parentdir_prefix,
- "VERSIONFILE_SOURCE": cfg.versionfile_source,
- })
- cmds["build_exe"] = cmd_build_exe
- del cmds["build_py"]
-
- if 'py2exe' in sys.modules: # py2exe enabled?
- try:
- from py2exe.distutils_buildexe import py2exe as _py2exe # py3
- except ImportError:
- from py2exe.build_exe import py2exe as _py2exe # py2
-
- class cmd_py2exe(_py2exe):
- def run(self):
- root = get_root()
- cfg = get_config_from_root(root)
- versions = get_versions()
- target_versionfile = cfg.versionfile_source
- print("UPDATING %s" % target_versionfile)
- write_to_version_file(target_versionfile, versions)
-
- _py2exe.run(self)
- os.unlink(target_versionfile)
- with open(cfg.versionfile_source, "w") as f:
- LONG = LONG_VERSION_PY[cfg.VCS]
- f.write(LONG %
- {"DOLLAR": "$",
- "STYLE": cfg.style,
- "TAG_PREFIX": cfg.tag_prefix,
- "PARENTDIR_PREFIX": cfg.parentdir_prefix,
- "VERSIONFILE_SOURCE": cfg.versionfile_source,
- })
- cmds["py2exe"] = cmd_py2exe
-
- # we override different "sdist" commands for both environments
- if "setuptools" in sys.modules:
- from setuptools.command.sdist import sdist as _sdist
- else:
- from distutils.command.sdist import sdist as _sdist
-
- class cmd_sdist(_sdist):
- def run(self):
- versions = get_versions()
- self._versioneer_generated_versions = versions
- # unless we update this, the command will keep using the old
- # version
- self.distribution.metadata.version = versions["version"]
- return _sdist.run(self)
-
- def make_release_tree(self, base_dir, files):
- root = get_root()
- cfg = get_config_from_root(root)
- _sdist.make_release_tree(self, base_dir, files)
- # now locate _version.py in the new base_dir directory
- # (remembering that it may be a hardlink) and replace it with an
- # updated value
- target_versionfile = os.path.join(base_dir, cfg.versionfile_source)
- print("UPDATING %s" % target_versionfile)
- write_to_version_file(target_versionfile,
- self._versioneer_generated_versions)
- cmds["sdist"] = cmd_sdist
-
- return cmds
-
-
-CONFIG_ERROR = """
-setup.cfg is missing the necessary Versioneer configuration. You need
-a section like:
-
- [versioneer]
- VCS = git
- style = pep440
- versionfile_source = src/myproject/_version.py
- versionfile_build = myproject/_version.py
- tag_prefix =
- parentdir_prefix = myproject-
-
-You will also need to edit your setup.py to use the results:
-
- import versioneer
- setup(version=versioneer.get_version(),
- cmdclass=versioneer.get_cmdclass(), ...)
-
-Please read the docstring in ./versioneer.py for configuration instructions,
-edit setup.cfg, and re-run the installer or 'python versioneer.py setup'.
-"""
-
-SAMPLE_CONFIG = """
-# See the docstring in versioneer.py for instructions. Note that you must
-# re-run 'versioneer.py setup' after changing this section, and commit the
-# resulting files.
-
-[versioneer]
-#VCS = git
-#style = pep440
-#versionfile_source =
-#versionfile_build =
-#tag_prefix =
-#parentdir_prefix =
-
-"""
-
-INIT_PY_SNIPPET = """
-from ._version import get_versions
-__version__ = get_versions()['version']
-del get_versions
-"""
-
-
-def do_setup():
- """Main VCS-independent setup function for installing Versioneer."""
- root = get_root()
- try:
- cfg = get_config_from_root(root)
- except (EnvironmentError, configparser.NoSectionError,
- configparser.NoOptionError) as e:
- if isinstance(e, (EnvironmentError, configparser.NoSectionError)):
- print("Adding sample versioneer config to setup.cfg",
- file=sys.stderr)
- with open(os.path.join(root, "setup.cfg"), "a") as f:
- f.write(SAMPLE_CONFIG)
- print(CONFIG_ERROR, file=sys.stderr)
- return 1
-
- print(" creating %s" % cfg.versionfile_source)
- with open(cfg.versionfile_source, "w") as f:
- LONG = LONG_VERSION_PY[cfg.VCS]
- f.write(LONG % {"DOLLAR": "$",
- "STYLE": cfg.style,
- "TAG_PREFIX": cfg.tag_prefix,
- "PARENTDIR_PREFIX": cfg.parentdir_prefix,
- "VERSIONFILE_SOURCE": cfg.versionfile_source,
- })
-
- ipy = os.path.join(os.path.dirname(cfg.versionfile_source),
- "__init__.py")
- if os.path.exists(ipy):
- try:
- with open(ipy, "r") as f:
- old = f.read()
- except EnvironmentError:
- old = ""
- if INIT_PY_SNIPPET not in old:
- print(" appending to %s" % ipy)
- with open(ipy, "a") as f:
- f.write(INIT_PY_SNIPPET)
- else:
- print(" %s unmodified" % ipy)
- else:
- print(" %s doesn't exist, ok" % ipy)
- ipy = None
-
- # Make sure both the top-level "versioneer.py" and versionfile_source
- # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so
- # they'll be copied into source distributions. Pip won't be able to
- # install the package without this.
- manifest_in = os.path.join(root, "MANIFEST.in")
- simple_includes = set()
- try:
- with open(manifest_in, "r") as f:
- for line in f:
- if line.startswith("include "):
- for include in line.split()[1:]:
- simple_includes.add(include)
- except EnvironmentError:
- pass
- # That doesn't cover everything MANIFEST.in can do
- # (http://docs.python.org/2/distutils/sourcedist.html#commands), so
- # it might give some false negatives. Appending redundant 'include'
- # lines is safe, though.
- if "versioneer.py" not in simple_includes:
- print(" appending 'versioneer.py' to MANIFEST.in")
- with open(manifest_in, "a") as f:
- f.write("include versioneer.py\n")
- else:
- print(" 'versioneer.py' already in MANIFEST.in")
- if cfg.versionfile_source not in simple_includes:
- print(" appending versionfile_source ('%s') to MANIFEST.in" %
- cfg.versionfile_source)
- with open(manifest_in, "a") as f:
- f.write("include %s\n" % cfg.versionfile_source)
- else:
- print(" versionfile_source already in MANIFEST.in")
-
- # Make VCS-specific changes. For git, this means creating/changing
- # .gitattributes to mark _version.py for export-subst keyword
- # substitution.
- do_vcs_install(manifest_in, cfg.versionfile_source, ipy)
- return 0
-
-
-def scan_setup_py():
- """Validate the contents of setup.py against Versioneer's expectations."""
- found = set()
- setters = False
- errors = 0
- with open("setup.py", "r") as f:
- for line in f.readlines():
- if "import versioneer" in line:
- found.add("import")
- if "versioneer.get_cmdclass()" in line:
- found.add("cmdclass")
- if "versioneer.get_version()" in line:
- found.add("get_version")
- if "versioneer.VCS" in line:
- setters = True
- if "versioneer.versionfile_source" in line:
- setters = True
- if len(found) != 3:
- print("")
- print("Your setup.py appears to be missing some important items")
- print("(but I might be wrong). Please make sure it has something")
- print("roughly like the following:")
- print("")
- print(" import versioneer")
- print(" setup( version=versioneer.get_version(),")
- print(" cmdclass=versioneer.get_cmdclass(), ...)")
- print("")
- errors += 1
- if setters:
- print("You should remove lines like 'versioneer.VCS = ' and")
- print("'versioneer.versionfile_source = ' . This configuration")
- print("now lives in setup.cfg, and should be removed from setup.py")
- print("")
- errors += 1
- return errors
-
-
-if __name__ == "__main__":
- cmd = sys.argv[1]
- if cmd == "setup":
- errors = do_setup()
- errors += scan_setup_py()
- if errors:
- sys.exit(1)