diff --git a/.codecov.yml b/.codecov.yml index 5a94096..4af5eb2 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,14 +1,14 @@ coverage: status: - project: # more options at https://docs.codecov.com/docs/commit-status + project: # more options at https://docs.codecov.com/docs/commit-status default: target: auto # use the coverage from the base commit, fail if coverage is lower - threshold: 0% # allow the coverage to drop by + threshold: 0% # allow the coverage to drop by comment: layout: " diff, flags, files" behavior: default require_changes: false - require_base: false # [true :: must have a base report to post] - require_head: false # [true :: must have a head report to post] + require_base: false # [true :: must have a base report to post] + require_head: false # [true :: must have a head report to post] hide_project_coverage: false # [true :: only show coverage on the git diff aka patch coverage] diff --git a/.flake8 b/.flake8 index 2d2cb16..7b2865c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,5 @@ +# As of now, flake8 does not natively support configuration via pyproject.toml +# https://github.com/microsoft/vscode-flake8/issues/135 [flake8] exclude = .git, @@ -5,7 +7,7 @@ exclude = build, dist, doc/source/conf.py -max-line-length = 115 +max-line-length = 79 # Ignore some style 'errors' produced while formatting by 'black' # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#labels-why-pycodestyle-warnings extend-ignore = E203 diff --git a/.github/ISSUE_TEMPLATE/release_checklist.md b/.github/ISSUE_TEMPLATE/release_checklist.md index 0f56027..fa94779 100644 --- a/.github/ISSUE_TEMPLATE/release_checklist.md +++ b/.github/ISSUE_TEMPLATE/release_checklist.md @@ -6,30 +6,41 @@ labels: "release" assignees: "" --- -### PyPI/GitHub release checklist: +### PyPI/GitHub rc-release preparation checklist: - [ ] All PRs/issues attached to the release are merged. - [ ] All the badges on the README are passing. - [ ] License information is verified as correct. If you are unsure, please comment below. - [ ] Locally rendered documentation contains all appropriate pages, including API references (check no modules are - missing), tutorials, and other human written text is up-to-date with any changes in the code. -- [ ] Installation instructions in the README, documentation and on the website (e.g., diffpy.org) are updated. + missing), tutorials, and other human-written text is up-to-date with any changes in the code. +- [ ] Installation instructions in the README, documentation, and the website (e.g., diffpy.org) are updated. - [ ] Successfully run any tutorial examples or do functional testing with the latest Python version. - [ ] Grammar and writing quality are checked (no typos). +- [ ] Install `pip install build twine`, run `python -m build` and `twine check dist/*` to ensure that the package can be built and is correctly formatted for PyPI release. -Please mention @sbillinge here when you are ready for PyPI/GitHub release. Include any additional comments necessary, such as -version information and details about the pre-release here: +Please mention @sbillinge here when you are ready for PyPI/GitHub release. Include any additional comments necessary, such as version information and details about the pre-release here: -### conda-forge release checklist: +### PyPI/GitHub full-release preparation checklist: + +- [ ] Create a new conda environment and install the rc from PyPI (`pip install ==??`) +- [ ] License information on PyPI is correct. +- [ ] Docs are deployed successfully to `https://www.diffpy.org/`. +- [ ] Successfully run all tests, tutorial examples or do functional testing. + +Please let @sbillinge know that all checks are done and the package is ready for full release. + +### conda-forge release preparation checklist: +- [ ] Ensure that the full release has appeared on PyPI successfully. - [ ] New package dependencies listed in `conda.txt` and `test.txt` are added to `meta.yaml` in the feedstock. -- [ ] All relevant issues in the feedstock are addressed in the release PR. +- [ ] Close any open issues on the feedstock. Reach out to @bobleesj if you have questions. +- [ ] Tag @sbillinge and @bobleesj for conda-forge release. ### Post-release checklist -- [ ] Run tutorial examples and conduct functional testing using the installation guide in the README. Attach screenshots/results as comments. -- [ ] Documentation (README, tutorials, API references, and websites) is deployed without broken links or missing figures. +- [ ] Run tutorial examples and conduct functional testing using the installation guide in the README. Attach screenshots/results as comments. +- [ ] Documentation (README, tutorials, API references, and websites) is deployed without broken links or missing figures. diff --git a/.github/workflows/build-wheel-release-upload.yml b/.github/workflows/build-wheel-release-upload.yml index 3e78404..7f0e3d7 100644 --- a/.github/workflows/build-wheel-release-upload.yml +++ b/.github/workflows/build-wheel-release-upload.yml @@ -1,4 +1,4 @@ -name: Release (GitHub/PyPI) +name: Release (GitHub/PyPI) and Deploy Docs on: workflow_dispatch: @@ -11,6 +11,8 @@ jobs: uses: Billingegroup/release-scripts/.github/workflows/_build-wheel-release-upload.yml@v0 with: project: diffpy.labpdfproc + c_extension: false + github_admin_username: sbillinge secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} PAT_TOKEN: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml b/.github/workflows/matrix-and-codecov-on-merge-to-main.yml index 0cfe6cc..5a5883d 100644 --- a/.github/workflows/matrix-and-codecov-on-merge-to-main.yml +++ b/.github/workflows/matrix-and-codecov-on-merge-to-main.yml @@ -11,7 +11,7 @@ on: workflow_dispatch: jobs: - coverage: + matrix-coverage: defaults: run: shell: bash -l {0} diff --git a/.github/workflows/publish-docs-on-release.yml b/.github/workflows/publish-docs-on-release.yml new file mode 100644 index 0000000..7474399 --- /dev/null +++ b/.github/workflows/publish-docs-on-release.yml @@ -0,0 +1,12 @@ +name: Deploy Documentation on Release + +on: + workflow_dispatch: + +jobs: + docs: + uses: Billingegroup/release-scripts/.github/workflows/_publish-docs-on-release.yml@v0 + with: + project: diffpy.labpdfproc + c_extension: false + headless: false diff --git a/.gitignore b/.gitignore index a25212e..d418364 100644 --- a/.gitignore +++ b/.gitignore @@ -90,10 +90,3 @@ target/ # Ipython Notebook .ipynb_checkpoints - -# version information -setup.cfg -/src/diffpy/*/version.cfg - -# Rever -rever/ diff --git a/.isort.cfg b/.isort.cfg index e0926f4..86f162b 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,5 @@ [settings] -line_length = 115 +# Keep import statement below line_length character limit +line_length = 79 multi_line_output = 3 include_trailing_comma = True diff --git a/doc/source/conf.py b/doc/source/conf.py index 8c9095e..12c7366 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -221,7 +221,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ("index", "diffpy.labpdfproc.tex", "diffpy.labpdfproc Documentation", ab_authors, "manual"), + ( + "index", + "diffpy.labpdfproc.tex", + "diffpy.labpdfproc Documentation", + ab_authors, + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -249,7 +255,15 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "diffpy.labpdfproc", "diffpy.labpdfproc Documentation", ab_authors, 1)] +man_pages = [ + ( + "index", + "diffpy.labpdfproc", + "diffpy.labpdfproc Documentation", + ab_authors, + 1, + ) +] # If true, show URL addresses after external links. # man_show_urls = False diff --git a/news/recut.rst b/news/recut.rst new file mode 100644 index 0000000..93e880c --- /dev/null +++ b/news/recut.rst @@ -0,0 +1,23 @@ +**Added:** + +* no news: modified .github, .pre-commit-config.yaml and related files to follow new practice for commit and issues. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/pyproject.toml b/pyproject.toml index dfc0458..09a5bd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ ignore-words = ".codespell/ignore_words.txt" skip = "*.cif" [tool.black] -line-length = 115 +line-length = 79 include = '\.pyi?$' exclude = ''' /( diff --git a/src/diffpy/labpdfproc/__init__.py b/src/diffpy/labpdfproc/__init__.py index e6ec545..573b965 100644 --- a/src/diffpy/labpdfproc/__init__.py +++ b/src/diffpy/labpdfproc/__init__.py @@ -13,7 +13,8 @@ # ############################################################################## -"""Tools for processing x-ray powder diffraction data from laboratory sources.""" +"""Tools for processing x-ray powder diffraction data +from laboratory sources.""" # package version from diffpy.labpdfproc.version import __version__ diff --git a/src/diffpy/labpdfproc/functions.py b/src/diffpy/labpdfproc/functions.py index 13e783d..d608543 100644 --- a/src/diffpy/labpdfproc/functions.py +++ b/src/diffpy/labpdfproc/functions.py @@ -10,7 +10,8 @@ RADIUS_MM = 1 N_POINTS_ON_DIAMETER = 300 TTH_GRID = np.arange(1, 180.1, 0.1) -# Round down the last element if it's slightly above 180 due to floating point precision +# Round down the last element if it's slightly above 180.00 +# due to floating point precision TTH_GRID[-1] = 180.00 CVE_METHODS = ["brute_force", "polynomial_interpolation"] @@ -18,12 +19,18 @@ MUD_LIST = [0.5, 1, 2, 3, 4, 5, 6] CWD = Path(__file__).parent.resolve() MULS = np.loadtxt(CWD / "data" / "inverse_cve.xy") -COEFFICIENT_LIST = np.array(pd.read_csv(CWD / "data" / "coefficient_list.csv", header=None)) -INTERPOLATION_FUNCTIONS = [interp1d(MUD_LIST, coefficients, kind="quadratic") for coefficients in COEFFICIENT_LIST] +COEFFICIENT_LIST = np.array( + pd.read_csv(CWD / "data" / "coefficient_list.csv", header=None) +) +INTERPOLATION_FUNCTIONS = [ + interp1d(MUD_LIST, coeffs, kind="quadratic") for coeffs in COEFFICIENT_LIST +] class Gridded_circle: - def __init__(self, radius=1, n_points_on_diameter=N_POINTS_ON_DIAMETER, mu=None): + def __init__( + self, radius=1, n_points_on_diameter=N_POINTS_ON_DIAMETER, mu=None + ): self.radius = radius self.npoints = n_points_on_diameter self.mu = mu @@ -32,17 +39,23 @@ def __init__(self, radius=1, n_points_on_diameter=N_POINTS_ON_DIAMETER, mu=None) self._get_grid_points() def _get_grid_points(self): - """Given a radius and a grid size, return a grid of points to uniformly sample that circle.""" + """Given a radius and a grid size, + return a grid of points to uniformly sample that circle.""" xs = np.linspace(-self.radius, self.radius, self.npoints) ys = np.linspace(-self.radius, self.radius, self.npoints) - self.grid = {(x, y) for x in xs for y in ys if x**2 + y**2 <= self.radius**2} + self.grid = { + (x, y) for x in xs for y in ys if x**2 + y**2 <= self.radius**2 + } self.total_points_in_grid = len(self.grid) def _get_entry_exit_coordinates(self, coordinate, angle): - """Get the coordinates where the beam enters and leaves the circle for a given angle and grid point. + """Get the coordinates where the beam enters and leaves the circle + for a given angle and grid point. It is calculated in the following way: - For the entry coordinate, the y-component will be the y of the grid point and the x-component will be minus + For the entry coordinate, + the y-component will be the y of the grid point + and the x-component will be minus the value of x on the circle at the height of this y. For the exit coordinate: @@ -53,7 +66,8 @@ def _get_entry_exit_coordinates(self, coordinate, angle): x^2 + (ax+b)^2 = r^2 => x^2 + a^2x^2 + 2abx + b^2 - r^2 = 0 => (1+a^2) x^2 + 2abx + (b^2 - r^2) = 0 - to find x_exit we find the roots of these equations and pick the root that is above y-grid + to find x_exit we find the roots of these equations + and pick the root that is above y-grid then we get y_exit from y_exit = a*x_exit + b. Parameters @@ -67,8 +81,10 @@ def _get_entry_exit_coordinates(self, coordinate, angle): Returns ------- (entry_point, exit_point): tuple of floats - (1) The coordinate of the entry point and (2) of the exit point of a beam entering horizontally - impinging on a coordinate point that lies in the circle and then exiting at some angle, angle. + (1) The coordinate of the entry point and + (2) of the exit point of a beam entering horizontally + impinging on a coordinate point that lies in the circle + and then exiting at some angle, angle. """ epsilon = 1e-7 # precision close to 90 angle = math.radians(angle) @@ -80,7 +96,9 @@ def _get_entry_exit_coordinates(self, coordinate, angle): if not math.isclose(angle, math.pi / 2, abs_tol=epsilon): b = ygrid - xgrid * math.tan(angle) a = math.tan(angle) - xexit_root1, xexit_root2 = np.roots((1 + a**2, 2 * a * b, b**2 - self.radius**2)) + xexit_root1, xexit_root2 = np.roots( + (1 + a**2, 2 * a * b, b**2 - self.radius**2) + ) yexit_root1 = a * xexit_root1 + b yexit_root2 = a * xexit_root2 + b if yexit_root2 >= yexit_root1: # We pick the point above @@ -93,8 +111,9 @@ def _get_entry_exit_coordinates(self, coordinate, angle): return entry_point, exit_point def _get_path_length(self, grid_point, angle): - """Return the path length of a horizontal line entering the circle at the - same height to the grid point then exiting at angle. + """Return the path length of + a horizontal line entering the circle at the same height + to the grid point then exiting at angle. Parameters ---------- @@ -107,10 +126,12 @@ def _get_path_length(self, grid_point, angle): Returns ------- (total distance, primary distance, secondary distance): tuple of floats - The tuple containing three floats, which are the total distance, entry distance and exit distance. + The tuple containing three floats, + which are the total distance, entry distance and exit distance. """ - # move angle a tad above zero if it is zero to avoid it having the wrong sign due to some rounding error + # move angle a tad above zero if it is zero + # to avoid it having the wrong sign due to some rounding error angle_delta = 0.000001 if angle == float(0): angle = angle + angle_delta @@ -121,14 +142,17 @@ def _get_path_length(self, grid_point, angle): return total_distance, primary_distance, secondary_distance def set_distances_at_angle(self, angle): - """Given an angle, set the distances from the grid points to the entry and exit coordinates. + """Given an angle, set the distances from the grid points + to the entry and exit coordinates. Parameters ---------- angle : float The angle of the output beam in degrees. """ - self.primary_distances, self.secondary_distances, self.distances = [], [], [] + self.primary_distances = [] + self.secondary_distances = [] + self.distances = [] for coord in self.grid: distance, primary, secondary = self._get_path_length(coord, angle) self.distances.append(distance) @@ -152,7 +176,8 @@ def set_muls_at_angle(self, angle): def _cve_brute_force(input_pattern, mud): - """Compute cve for the given mud on a global grid using the brute-force method. + """Compute cve for the given mud on a global grid + using the brute-force method. Assume mu=mud/2, given that the same mu*D yields the same cve and D/2=1. """ mu_sample_invmm = mud / 2 @@ -185,13 +210,22 @@ def _cve_polynomial_interpolation(input_pattern, mud): """ if mud > 6 or mud < 0.5: raise ValueError( - f"mu*D is out of the acceptable range (0.5 to 6) for polynomial interpolation. " - f"Please rerun with a value within this range or specifying another method from {*CVE_METHODS, }." + f"mu*D is out of the acceptable range (0.5 to 6) " + f"for polynomial interpolation. " + f"Please rerun with a value within this range " + f"or specifying another method from {*CVE_METHODS, }." ) coeff_a, coeff_b, coeff_c, coeff_d, coeff_e = [ - interpolation_function(mud) for interpolation_function in INTERPOLATION_FUNCTIONS + interpolation_function(mud) + for interpolation_function in INTERPOLATION_FUNCTIONS ] - muls = np.array(coeff_a * MULS**4 + coeff_b * MULS**3 + coeff_c * MULS**2 + coeff_d * MULS + coeff_e) + muls = np.array( + coeff_a * MULS**4 + + coeff_b * MULS**3 + + coeff_c * MULS**2 + + coeff_d * MULS + + coeff_e + ) cve = 1 / muls cve_do = DiffractionObject( @@ -213,21 +247,30 @@ def _cve_method(method): "polynomial_interpolation": _cve_polynomial_interpolation, } if method not in CVE_METHODS: - raise ValueError(f"Unknown method: {method}. Allowed methods are {*CVE_METHODS, }.") + raise ValueError( + f"Unknown method: {method}. " + f"Allowed methods are {*CVE_METHODS, }." + ) return methods[method] -def compute_cve(input_pattern, mud, method="polynomial_interpolation", xtype="tth"): - f"""Compute and interpolate the cve for the given input diffraction data and mu*D using the selected method. +def compute_cve( + input_pattern, mud, method="polynomial_interpolation", xtype="tth" +): + f"""Compute and interpolate the cve + for the given input diffraction data and mu*D + using the selected method. Parameters ---------- input_pattern : DiffractionObject The input diffraction object to which the cve will be applied. mud : float - The mu*D value of the diffraction object, where D is the diameter of the circle. + The mu*D value of the diffraction object, + where D is the diameter of the circle. xtype : str - The quantity on the independent variable axis, allowed values are {*XQUANTITIES, }. + The quantity on the independent variable axis, + allowed values are {*XQUANTITIES, }. method : str The method used to calculate cve, must be one of {*CVE_METHODS, }. @@ -255,7 +298,8 @@ def compute_cve(input_pattern, mud, method="polynomial_interpolation", xtype="tt def apply_corr(input_pattern, absorption_correction): - """Apply absorption correction to the given diffraction object with the correction diffraction object. + """Apply absorption correction to the given diffraction object + with the correction diffraction object. Parameters ---------- @@ -267,7 +311,8 @@ def apply_corr(input_pattern, absorption_correction): Returns ------- corrected_pattern: DiffractionObject - The corrected diffraction object with the correction applied through multiplication. + The corrected diffraction object + with the correction applied through multiplication. """ corrected_pattern = input_pattern * absorption_correction return corrected_pattern diff --git a/src/diffpy/labpdfproc/labpdfprocapp.py b/src/diffpy/labpdfproc/labpdfprocapp.py index 2848ef3..1d6581f 100644 --- a/src/diffpy/labpdfproc/labpdfprocapp.py +++ b/src/diffpy/labpdfproc/labpdfprocapp.py @@ -4,7 +4,11 @@ from gooey import Gooey, GooeyParser from diffpy.labpdfproc.functions import CVE_METHODS, apply_corr, compute_cve -from diffpy.labpdfproc.tools import known_sources, load_metadata, preprocessing_args +from diffpy.labpdfproc.tools import ( + known_sources, + load_metadata, + preprocessing_args, +) from diffpy.utils.diffraction_objects import XQUANTITIES, DiffractionObject from diffpy.utils.parsers.loaddata import loadData @@ -20,17 +24,22 @@ def define_arguments(): "name": ["input"], "help": ( "The filename(s) or folder(s) of the datafile(s) to load. " - "Required.\nSupply a space-separated list of files or directories. " - "Long lists can be supplied, one per line, in a file with name " - "file_list.txt. If one or more directory is provided, all valid " - "data-files in that directory will be processed. Examples of valid " - "inputs are 'file.xy', 'data/file.xy', 'file.xy, data/file.xy', " - "'.' (load everything in the current directory), 'data' (load " - "everything in the folder ./data), 'data/file_list.txt' (load " - "the list of files contained in the text-file called " - "file_list.txt that can be found in the folder ./data), " - "'./*.chi', 'data/*.chi' (load all files with extension .chi in the " - "folder ./data)." + "Required.\n" + "Supply a space-separated list of files or directories. " + "Long lists can be supplied, one per line, " + "in a file with name file_list.txt. " + "If one or more directory is provided, all valid " + "data-files in that directory will be processed. " + "Examples of valid " + "inputs are 'file.xy', 'data/file.xy', " + "'file.xy, data/file.xy', " + "'.' (load everything in the current directory), " + "'data' (load everything in the folder ./data), " + "'data/file_list.txt' (load the list of files " + "contained in the text-file called file_list.txt " + "that can be found in the folder ./data), " + "'./*.chi', 'data/*.chi' " + "(load all files with extension .chi in the folder ./data)." ), "nargs": "+", "widget": "MultiFileChooser", @@ -38,24 +47,28 @@ def define_arguments(): { "name": ["-a", "--anode-type"], "help": ( - f"The type of the x-ray source. Allowed values are " - f"{*[known_sources], }. Either specify a known x-ray source or specify wavelength." + f"The type of the x-ray source. " + f"Allowed values are {*[known_sources], }. " + f"Either specify a known x-ray source or specify wavelength." ), "default": "Mo", }, { "name": ["-w", "--wavelength"], "help": ( - "X-ray source wavelength in angstroms. Not needed if the anode-type " - "is specified. This wavelength will override the anode wavelength if both are specified." + "X-ray source wavelength in angstroms. " + "Not needed if the anode-type is specified. " + "This wavelength will override the anode wavelength " + "if both are specified." ), "type": float, }, { "name": ["-o", "--output-directory"], "help": ( - "The name of the output directory. If not specified " - "then corrected files will be written to the current directory. " + "The name of the output directory. " + "If not specified then corrected files will be " + "written to the current directory. " "If the specified directory doesn't exist it will be created." ), "default": None, @@ -63,8 +76,9 @@ def define_arguments(): { "name": ["-x", "--xtype"], "help": ( - f"The quantity on the independent variable axis. Allowed " - f"values: {*XQUANTITIES, }. If not specified then two-theta " + f"The quantity on the independent variable axis. " + f"Allowed values: {*XQUANTITIES, }. " + f"If not specified then two-theta " f"is assumed for the independent variable." ), "default": "tth", @@ -72,34 +86,42 @@ def define_arguments(): { "name": ["-c", "--output-correction"], "help": ( - "The absorption correction will be output to a file if this " - "flag is set. Default is that it is not output." + "The absorption correction will be output to a file " + "if this flag is set. " + "Default is that it is not output." ), "action": "store_true", }, { "name": ["-f", "--force-overwrite"], - "help": "Outputs will not overwrite existing file unless --force is specified.", + "help": "Outputs will not overwrite existing file " + "unless --force is specified.", "action": "store_true", }, { "name": ["-m", "--method"], "help": ( - f"The method for computing absorption correction. Allowed methods: {*CVE_METHODS, }. " - f"Default method is polynomial interpolation if not specified. " + f"The method for computing absorption correction. " + f"Allowed methods: {*CVE_METHODS, }. " + f"Default method is polynomial interpolation " + f"if not specified. " ), "default": "polynomial_interpolation", }, { "name": ["-u", "--user-metadata"], "help": ( - "Specify key-value pairs to be loaded into metadata using the format key=value. " - "Separate pairs with whitespace, and ensure no whitespaces before or after the = sign. " + "Specify key-value pairs to be loaded into metadata " + "using the format key=value. " + "Separate pairs with whitespace, " + "and ensure no whitespaces before or after the = sign. " "Avoid using = in keys. If multiple = signs are present, " "only the first separates the key and value. " "If a key or value contains whitespace, enclose it in quotes. " - "For example, facility='NSLS II', 'facility=NSLS II', beamline=28ID-2, " - "'beamline'='28ID-2', 'favorite color'=blue, are all valid key=value items. " + "For example, facility='NSLS II', " + "'facility=NSLS II', beamline=28ID-2, " + "'beamline'='28ID-2', 'favorite color'=blue, " + "are all valid key=value items. " ), "nargs": "+", "metavar": "KEY=VALUE", @@ -107,30 +129,34 @@ def define_arguments(): { "name": ["-n", "--username"], "help": ( - "Username will be loaded from config files. Specify here " - "only if you want to override that behavior at runtime. " + "Username will be loaded from config files. " + "Specify here only if you want to " + "override that behavior at runtime. " ), "default": None, }, { "name": ["-e", "--email"], "help": ( - "Email will be loaded from config files. Specify here " - "only if you want to override that behavior at runtime. " + "Email will be loaded from config files. " + "Specify here only if you want to " + "override that behavior at runtime. " ), "default": None, }, { "name": ["--orcid"], "help": ( - "ORCID will be loaded from config files. Specify here " - "only if you want to override that behavior at runtime. " + "ORCID will be loaded from config files. " + "Specify here only if you want to " + "override that behavior at runtime. " ), "default": None, }, { "name": ["-z", "--z-scan-file"], - "help": "Path to the z-scan file to be loaded to determine the mu*D value", + "help": "Path to the z-scan file to be loaded " + "to determine the mu*D value.", "default": None, "widget": "FileChooser", }, @@ -141,7 +167,11 @@ def define_arguments(): def get_args(override_cli_inputs=None): p = ArgumentParser() for arg in define_arguments(): - kwargs = {key: value for key, value in arg.items() if key != "name" and key != "widget"} + kwargs = { + key: value + for key, value in arg.items() + if key != "name" and key != "widget" + } p.add_argument(*arg["name"], **kwargs) args = p.parse_args(override_cli_inputs) return args @@ -158,7 +188,11 @@ def gooey_parser(): def main(): - args = gooey_parser() if len(sys.argv) == 1 or "--gui" in sys.argv else get_args() + args = ( + gooey_parser() + if len(sys.argv) == 1 or "--gui" in sys.argv + else get_args() + ) args = preprocessing_args(args) for filepath in args.input_paths: @@ -169,13 +203,18 @@ def main(): if outfile.exists() and not args.force_overwrite: sys.exit( - f"Output file {str(outfile)} already exists. Please rerun " - f"specifying -f if you want to overwrite it." + f"Output file {str(outfile)} already exists. " + f"Please rerun specifying -f if you want to overwrite it." ) - if corrfile.exists() and args.output_correction and not args.force_overwrite: + if ( + corrfile.exists() + and args.output_correction + and not args.force_overwrite + ): sys.exit( - f"Corrections file {str(corrfile)} was requested and already " - f"exists. Please rerun specifying -f if you want to overwrite it." + f"Corrections file {str(corrfile)} " + f"was requested and already exists. " + f"Please rerun specifying -f if you want to overwrite it." ) xarray, yarray = loadData(filepath, unpack=True) @@ -189,9 +228,13 @@ def main(): metadata=load_metadata(args, filepath), ) - absorption_correction = compute_cve(input_pattern, args.mud, args.method, args.xtype) + absorption_correction = compute_cve( + input_pattern, args.mud, args.method, args.xtype + ) corrected_data = apply_corr(input_pattern, absorption_correction) - corrected_data.name = f"Absorption corrected input_data: {input_pattern.name}" + corrected_data.name = ( + f"Absorption corrected input_data: " f"{input_pattern.name}" + ) corrected_data.dump(f"{outfile}", xtype=args.xtype) if args.output_correction: diff --git a/src/diffpy/labpdfproc/tools.py b/src/diffpy/labpdfproc/tools.py index 32cfaa2..d2743f6 100644 --- a/src/diffpy/labpdfproc/tools.py +++ b/src/diffpy/labpdfproc/tools.py @@ -1,15 +1,30 @@ import copy from pathlib import Path -from diffpy.utils.diffraction_objects import ANGLEQUANTITIES, QQUANTITIES, XQUANTITIES -from diffpy.utils.tools import check_and_build_global_config, compute_mud, get_package_info, get_user_info +from diffpy.utils.diffraction_objects import ( + ANGLEQUANTITIES, + QQUANTITIES, + XQUANTITIES, +) +from diffpy.utils.tools import ( + check_and_build_global_config, + compute_mud, + get_package_info, + get_user_info, +) WAVELENGTHS = {"Mo": 0.71073, "Ag": 0.59, "Cu": 1.5406} known_sources = [key for key in WAVELENGTHS.keys()] # Exclude wavelength from metadata to prevent duplication, # as the dump function in diffpy.utils writes it explicitly. -METADATA_KEYS_TO_EXCLUDE = ["output_correction", "force_overwrite", "input", "input_paths", "wavelength"] +METADATA_KEYS_TO_EXCLUDE = [ + "output_correction", + "force_overwrite", + "input", + "input_paths", + "wavelength", +] def set_output_directory(args): @@ -28,9 +43,14 @@ def set_output_directory(args): Returns ------- args : argparse.Namespace - The updated arguments, with output_directory as the full path to the output file directory. + The updated arguments, + with output_directory as the full path to the output file directory. """ - output_dir = Path(args.output_directory).resolve() if args.output_directory else Path.cwd().resolve() + output_dir = ( + Path(args.output_directory).resolve() + if args.output_directory + else Path.cwd().resolve() + ) output_dir.mkdir(parents=True, exist_ok=True) args.output_directory = output_dir return args @@ -49,18 +69,23 @@ def _expand_user_input(args): args : argparse.Namespace The updated arguments with the modified input list. """ - file_list_inputs = [input_name for input_name in args.input if "file_list" in input_name] + file_list_inputs = [ + input_name for input_name in args.input if "file_list" in input_name + ] for file_list_input in file_list_inputs: with open(file_list_input, "r") as f: file_inputs = [input_name.strip() for input_name in f.readlines()] args.input.extend(file_inputs) args.input.remove(file_list_input) - wildcard_inputs = [input_name for input_name in args.input if "*" in input_name] + wildcard_inputs = [ + input_name for input_name in args.input if "*" in input_name + ] for wildcard_input in wildcard_inputs: input_files = [ str(file) for file in Path(".").glob(wildcard_input) - if "file_list" not in file.name and "diffpyconfig.json" not in file.name + if "file_list" not in file.name + and "diffpyconfig.json" not in file.name ] args.input.extend(input_files) args.input.remove(wildcard_input) @@ -68,8 +93,10 @@ def _expand_user_input(args): def set_input_lists(args): - """Set input directory and files. It takes cli inputs, checks if they are files or directories and creates - a list of files to be processed which is stored in the args Namespace. + """Set input directory and files. + It takes cli inputs, checks if they are files or directories + and creates a list of files to be processed + which is stored in the args Namespace. Parameters ---------- @@ -99,23 +126,28 @@ def set_input_lists(args): input_files = [ file.resolve() for file in input_files - if file.is_file() and "file_list" not in file.name and "diffpyconfig.json" not in file.name + if file.is_file() + and "file_list" not in file.name + and "diffpyconfig.json" not in file.name ] input_paths.extend(input_files) else: raise FileNotFoundError( - f"Cannot find {input_name}. Please specify valid input file(s) or directories." + f"Cannot find {input_name}. " + f"Please specify valid input file(s) or directories." ) else: raise FileNotFoundError( - f"Cannot find {input_name}. Please specify valid input file(s) or directories." + f"Cannot find {input_name}. " + f"Please specify valid input file(s) or directories." ) setattr(args, "input_paths", list(set(input_paths))) return args def set_wavelength(args): - """Set the wavelength based on the given anode_type. If a wavelength is provided, + """Set the wavelength based on the given anode_type. + If a wavelength is provided, it will be used, and the anode_type argument will be removed. Parameters @@ -126,7 +158,8 @@ def set_wavelength(args): Raises ------ ValueError - Raised when input wavelength is non-positive or if input anode_type is not one of the known sources. + Raised when input wavelength is non-positive + or if input anode_type is not one of the known sources. Returns ------- @@ -135,11 +168,18 @@ def set_wavelength(args): """ if args.wavelength is not None and args.wavelength <= 0: raise ValueError( - "No valid wavelength. Please rerun specifying a known anode_type or a positive wavelength." + "No valid wavelength. " + "Please rerun specifying a known anode_type " + "or a positive wavelength." ) - if not args.wavelength and args.anode_type and args.anode_type not in WAVELENGTHS: + if ( + not args.wavelength + and args.anode_type + and args.anode_type not in WAVELENGTHS + ): raise ValueError( - f"Anode type not recognized. Please rerun specifying an anode_type from {*known_sources, }." + f"Anode type not recognized. " + f"Please rerun specifying an anode_type from {*known_sources, }." ) if args.wavelength: @@ -152,7 +192,8 @@ def set_wavelength(args): def set_xtype(args): - f"""Set the xtype based on the given input arguments, raise an error if xtype is not one of {*XQUANTITIES, }. + f"""Set the xtype based on the given input arguments, + raise an error if xtype is not one of {*XQUANTITIES, }. Parameters ---------- @@ -165,9 +206,14 @@ def set_xtype(args): The updated arguments with the xtype as one of q, tth, or d. """ if args.xtype.lower() not in XQUANTITIES: - raise ValueError(f"Unknown xtype: {args.xtype}. Allowed xtypes are {*XQUANTITIES, }.") + raise ValueError( + f"Unknown xtype: {args.xtype}. " + f"Allowed xtypes are {*XQUANTITIES, }." + ) args.xtype = ( - "q" if args.xtype.lower() in QQUANTITIES else "tth" if args.xtype.lower() in ANGLEQUANTITIES else "d" + "q" + if args.xtype.lower() in QQUANTITIES + else "tth" if args.xtype.lower() in ANGLEQUANTITIES else "d" ) return args @@ -188,7 +234,10 @@ def set_mud(args): if args.z_scan_file: filepath = Path(args.z_scan_file).resolve() if not filepath.is_file(): - raise FileNotFoundError(f"Cannot find {args.z_scan_file}. Please specify a valid file path.") + raise FileNotFoundError( + f"Cannot find {args.z_scan_file}. " + f"Please specify a valid file path." + ) args.z_scan_file = str(filepath) args.mud = compute_mud(filepath) return args @@ -203,7 +252,8 @@ def _load_key_value_pair(s): def load_user_metadata(args): - """Load user metadata into args, raise ValueError if it is in incorrect format. + """Load user metadata into args, + raise ValueError if it is in incorrect format. Parameters ---------- @@ -213,7 +263,8 @@ def load_user_metadata(args): Returns ------- args : argparse.Namespace - The updated argparse Namespace with user metadata inserted as key-value pairs. + The updated argparse Namespace + with user metadata inserted as key-value pairs. """ reserved_keys = set(vars(args).keys()) @@ -227,17 +278,24 @@ def load_user_metadata(args): ) key, value = _load_key_value_pair(item) if key in reserved_keys: - raise ValueError(f"{key} is a reserved name. Please rerun using a different key name.") + raise ValueError( + f"{key} is a reserved name. " + f"Please rerun using a different key name." + ) if hasattr(args, key): - raise ValueError(f"Please do not specify repeated keys: {key}.") + raise ValueError( + f"Please do not specify repeated keys: " f"{key}." + ) setattr(args, key, value) delattr(args, "user_metadata") return args def load_user_info(args): - """Load user info into args. If none is provided, call check_and_build_global_config function from - diffpy.utils to prompt the user for inputs. Otherwise, call get_user_info with the provided arguments. + """Load user info into args. + If none is provided, call check_and_build_global_config function + from diffpy.utils to prompt the user for inputs. + Otherwise, call get_user_info with the provided arguments. Parameters ---------- @@ -247,11 +305,16 @@ def load_user_info(args): Returns ------- args : argparse.Namespace - The updated argparse Namespace with username, email, and orcid inserted. + The updated argparse Namespace + with username, email, and orcid inserted. """ if args.username is None or args.email is None: check_and_build_global_config() - config = get_user_info(owner_name=args.username, owner_email=args.email, owner_orcid=args.orcid) + config = get_user_info( + owner_name=args.username, + owner_email=args.email, + owner_orcid=args.orcid, + ) args.username = config.get("owner_name") args.email = config.get("owner_email") args.orcid = config.get("owner_orcid") @@ -259,7 +322,8 @@ def load_user_info(args): def load_package_info(args): - """Load diffpy.labpdfproc package name and version into args using get_package_info function from diffpy.utils. + """Load diffpy.labpdfproc package name and version into args + using get_package_info function from diffpy.utils. Parameters ---------- @@ -269,7 +333,8 @@ def load_package_info(args): Returns ------- args : argparse.Namespace - The updated argparse Namespace with diffpy.labpdfproc name and version inserted. + The updated argparse Namespace + with diffpy.labpdfproc name and version inserted. """ metadata = get_package_info("diffpy.labpdfproc") setattr(args, "package_info", metadata["package_info"]) @@ -303,7 +368,8 @@ def preprocessing_args(args): def load_metadata(args, filepath): - """Load the relevant metadata from args to write into the header of the output files. + """Load the relevant metadata from args + to write into the header of the output files. Parameters ---------- diff --git a/tests/conftest.py b/tests/conftest.py index d3b4bdc..2dcb4a5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,9 +14,14 @@ def user_filesystem(tmp_path): test_dir = base_dir / "test_dir" test_dir.mkdir(parents=True, exist_ok=True) - chi_data = "dataformat = twotheta\n mode = xray\n # chi_Q chi_I\n 1 2\n 3 4\n 5 6\n 7 8\n" + chi_data = ( + "dataformat = twotheta\n mode = " + "xray\n # chi_Q chi_I\n 1 2\n 3 4\n 5 6\n 7 8\n" + ) xy_data = "1 2\n 3 4\n 5 6\n 7 8" - unreadable_data = "This is a file with no data that is non-readable by " "LoadData" + unreadable_data = ( + "This is a file with no data " "that is non-readable by LoadData" + ) binary_data = b"\x00\x01\x02\x03\x04" with open(base_dir / "good_data.chi", "w") as f: @@ -42,7 +47,12 @@ def user_filesystem(tmp_path): f.write(binary_data) with open(input_dir / "file_list.txt", "w") as f: - f.write("good_data.chi \n good_data.xy \n good_data.txt \n missing_file.txt") + f.write( + "good_data.chi" + " \n good_data.xy" + " \n good_data.txt" + " \n missing_file.txt" + ) with open(input_dir / "file_list_example2.txt", "w") as f: f.write("input_dir/*.txt \n") f.write("input_dir/good_data.chi \n") diff --git a/tests/test_functions.py b/tests/test_functions.py index 6691f73..ced10c1 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -3,7 +3,12 @@ import numpy as np import pytest -from diffpy.labpdfproc.functions import CVE_METHODS, Gridded_circle, apply_corr, compute_cve +from diffpy.labpdfproc.functions import ( + CVE_METHODS, + Gridded_circle, + apply_corr, + compute_cve, +) from diffpy.utils.diffraction_objects import DiffractionObject @@ -16,28 +21,52 @@ ), ( {"radius": 1, "n_points_on_diameter": 4, "mu": 1}, - {(-0.333333, -0.333333), (0.333333, -0.333333), (-0.333333, 0.333333), (0.333333, 0.333333)}, + { + (-0.333333, -0.333333), + (0.333333, -0.333333), + (-0.333333, 0.333333), + (0.333333, 0.333333), + }, ), ], ) def test_get_grid_points(inputs, expected_grid): actual_gs = Gridded_circle( - radius=inputs["radius"], n_points_on_diameter=inputs["n_points_on_diameter"], mu=inputs["mu"] + radius=inputs["radius"], + n_points_on_diameter=inputs["n_points_on_diameter"], + mu=inputs["mu"], ) actual_grid_sorted = sorted(actual_gs.grid) expected_grid_sorted = sorted(expected_grid) - for actual_point, expected_point in zip(actual_grid_sorted, expected_grid_sorted): - assert actual_point == pytest.approx(expected_point, rel=1e-4, abs=1e-6) + for actual_pt, expected_pt in zip( + actual_grid_sorted, expected_grid_sorted + ): + assert actual_pt == pytest.approx(expected_pt, rel=1e-4, abs=1e-6) @pytest.mark.parametrize( "inputs, expected_distances", [ - ({"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 45}, [0, 1.4142135, 1.4142135, 2, 2]), - ({"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 90}, [0, 0, 2, 2, 2]), - ({"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 120}, [0, 0, 2, 3, 1.73205]), - ({"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 30}, [2.057347, 2.044451, 1.621801, 1.813330]), - ({"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 90}, [1.885618, 1.885618, 2.552285, 1.218951]), + ( + {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 45}, + [0, 1.4142135, 1.4142135, 2, 2], + ), + ( + {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 90}, + [0, 0, 2, 2, 2], + ), + ( + {"radius": 1, "n_points_on_diameter": 3, "mu": 1, "angle": 120}, + [0, 0, 2, 3, 1.73205], + ), + ( + {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 30}, + [2.057347, 2.044451, 1.621801, 1.813330], + ), + ( + {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 90}, + [1.885618, 1.885618, 2.552285, 1.218951], + ), ( {"radius": 1, "n_points_on_diameter": 4, "mu": 1, "angle": 140}, [1.139021, 2.200102, 2.744909, 1.451264], @@ -46,12 +75,16 @@ def test_get_grid_points(inputs, expected_grid): ) def test_set_distances_at_angle(inputs, expected_distances): actual_gs = Gridded_circle( - radius=inputs["radius"], n_points_on_diameter=inputs["n_points_on_diameter"], mu=inputs["mu"] + radius=inputs["radius"], + n_points_on_diameter=inputs["n_points_on_diameter"], + mu=inputs["mu"], ) actual_gs.set_distances_at_angle(inputs["angle"]) actual_distances_sorted = sorted(actual_gs.distances) expected_distances_sorted = sorted(expected_distances) - assert actual_distances_sorted == pytest.approx(expected_distances_sorted, rel=1e-4, abs=1e-6) + assert actual_distances_sorted == pytest.approx( + expected_distances_sorted, rel=1e-4, abs=1e-6 + ) @pytest.mark.parametrize( @@ -66,16 +99,29 @@ def test_set_muls_at_angle(input_mu, expected_muls): actual_gs.set_muls_at_angle(120) actual_muls_sorted = sorted(actual_gs.muls) expected_muls_sorted = sorted(expected_muls) - assert actual_muls_sorted == pytest.approx(expected_muls_sorted, rel=1e-4, abs=1e-6) + assert actual_muls_sorted == pytest.approx( + expected_muls_sorted, rel=1e-4, abs=1e-6 + ) @pytest.mark.parametrize( "input_xtype, expected", [ - ("tth", {"xarray": np.array([90, 90.1, 90.2]), "yarray": np.array([0.5, 0.5, 0.5]), "xtype": "tth"}), + ( + "tth", + { + "xarray": np.array([90, 90.1, 90.2]), + "yarray": np.array([0.5, 0.5, 0.5]), + "xtype": "tth", + }, + ), ( "q", - {"xarray": np.array([5.76998, 5.77501, 5.78004]), "yarray": np.array([0.5, 0.5, 0.5]), "xtype": "q"}, + { + "xarray": np.array([5.76998, 5.77501, 5.78004]), + "yarray": np.array([0.5, 0.5, 0.5]), + "xtype": "q", + }, ), ], ) @@ -92,7 +138,12 @@ def test_compute_cve(input_xtype, expected, mocker): name="test", metadata={"thing1": 1, "thing2": "thing2"}, ) - actual_cve_do = compute_cve(input_pattern, mud=1, method="polynomial_interpolation", xtype=input_xtype) + actual_cve_do = compute_cve( + input_pattern, + mud=1, + method="polynomial_interpolation", + xtype=input_xtype, + ) expected_cve_do = DiffractionObject( xarray=expected["xarray"], yarray=expected["yarray"], @@ -110,16 +161,20 @@ def test_compute_cve(input_xtype, expected, mocker): [ ( {"mud": 7, "method": "polynomial_interpolation"}, - f"mu*D is out of the acceptable range (0.5 to 6) for polynomial interpolation. " - f"Please rerun with a value within this range or specifying another method from {*CVE_METHODS, }.", + f"mu*D is out of the acceptable range (0.5 to 6) " + f"for polynomial interpolation. " + f"Please rerun with a value within this range " + f"or specifying another method from {*CVE_METHODS, }.", ), ( {"mud": 1, "method": "invalid_method"}, - f"Unknown method: invalid_method. Allowed methods are {*CVE_METHODS, }.", + f"Unknown method: invalid_method. " + f"Allowed methods are {*CVE_METHODS, }.", ), ( {"mud": 7, "method": "invalid_method"}, - f"Unknown method: invalid_method. Allowed methods are {*CVE_METHODS, }.", + f"Unknown method: invalid_method. " + f"Allowed methods are {*CVE_METHODS, }.", ), ], ) diff --git a/tests/test_tools.py b/tests/test_tools.py index 778492b..0b296c2 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -24,22 +24,35 @@ @pytest.mark.parametrize( "inputs, expected", [ - # Use cases can be found here: https://github.com/diffpy/diffpy.labpdfproc/issues/48 - # This test covers existing single input file, directory, a file list, and multiple files - # We store absolute path into input_directory and file names into input_file - ( # C1: single good file in the current directory, expect to return the absolute Path of the file + # Use cases can be found here: + # https://github.com/diffpy/diffpy.labpdfproc/issues/48. + # This test covers existing single input file, directory, + # a file list, and multiple files. + # We store absolute path into input_directory + # and file names into input_file. + ( # C1: single good file in the current directory, + # expect to return the absolute Path of the file ["good_data.chi"], ["good_data.chi"], ), - ( # C2: single good file in an input directory, expect to return the absolute Path of the file + ( # C2: single good file in an input directory, + # expect to return the absolute Path of the file ["input_dir/good_data.chi"], ["input_dir/good_data.chi"], ), - ( # C3: glob current directory, expect to return all files in the current directory + ( # C3: glob current directory, + # expect to return all files in the current directory ["."], - ["good_data.chi", "good_data.xy", "good_data.txt", "unreadable_file.txt", "binary.pkl"], + [ + "good_data.chi", + "good_data.xy", + "good_data.txt", + "unreadable_file.txt", + "binary.pkl", + ], ), - ( # C4: glob input directory, expect to return all files in that directory + ( # C4: glob input directory, + # expect to return all files in that directory ["./input_dir"], [ "input_dir/good_data.chi", @@ -49,7 +62,8 @@ "input_dir/binary.pkl", ], ), - ( # C5: glob list of input directories, expect to return all files in the directories + ( # C5: glob list of input directories, + # expect to return all files in the directories [".", "./input_dir"], [ "./good_data.chi", @@ -64,7 +78,8 @@ "input_dir/binary.pkl", ], ), - ( # C6: file_list_example2.txt list of files provided in different directories with wildcard, + ( # C6: file_list_example2.txt + # list of files provided in different directories with wildcard, # expect to return all files listed on the file_list file ["input_dir/file_list_example2.txt"], [ @@ -74,15 +89,18 @@ "input_dir/unreadable_file.txt", ], ), - ( # C7: wildcard pattern, expect to match files with .chi extension in the same directory + ( # C7: wildcard pattern, + # expect to match files with .chi extension in the same directory ["./*.chi"], ["good_data.chi"], ), - ( # C8: wildcard pattern, expect to match files with .chi extension in the input directory + ( # C8: wildcard pattern, + # expect to match files with .chi extension in the input directory ["input_dir/*.chi"], ["input_dir/good_data.chi"], ), - ( # C9: wildcard pattern, expect to match files starting with good_data + ( # C9: wildcard pattern, + # expect to match files starting with good_data ["good_data*"], ["good_data.chi", "good_data.xy", "good_data.txt"], ), @@ -91,7 +109,9 @@ def test_set_input_lists(inputs, expected, user_filesystem): base_dir = Path(user_filesystem) os.chdir(base_dir) - expected_paths = [base_dir.resolve() / expected_path for expected_path in expected] + expected_paths = [ + base_dir.resolve() / expected_path for expected_path in expected + ] cli_inputs = ["2.5"] + inputs actual_args = get_args(cli_inputs) @@ -102,26 +122,37 @@ def test_set_input_lists(inputs, expected, user_filesystem): @pytest.mark.parametrize( "inputs, expected_error_msg", [ - # This test covers non-existing single input file or directory, in this case we raise an error with message + # This test covers non-existing single input file or directory, + # in this case we raise an error with message ( # C1: non-existing single file ["non_existing_file.xy"], - "Cannot find non_existing_file.xy. Please specify valid input file(s) or directories.", + "Cannot find non_existing_file.xy. " + "Please specify valid input file(s) or directories.", ), ( # C2: non-existing single file with directory ["./input_dir/non_existing_file.xy"], - "Cannot find ./input_dir/non_existing_file.xy. Please specify valid input file(s) or directories.", + "Cannot find ./input_dir/non_existing_file.xy. " + "Please specify valid input file(s) or directories.", ), ( # C3: non-existing single directory ["./non_existing_dir"], - "Cannot find ./non_existing_dir. Please specify valid input file(s) or directories.", + "Cannot find ./non_existing_dir. " + "Please specify valid input file(s) or directories.", ), ( # C4: list of files provided (with missing files) - ["good_data.chi", "good_data.xy", "unreadable_file.txt", "missing_file.txt"], - "Cannot find missing_file.txt. Please specify valid input file(s) or directories.", + [ + "good_data.chi", + "good_data.xy", + "unreadable_file.txt", + "missing_file.txt", + ], + "Cannot find missing_file.txt. " + "Please specify valid input file(s) or directories.", ), ( # C5: file_list.txt list of files provided (with missing files) ["input_dir/file_list.txt"], - "Cannot find missing_file.txt. Please specify valid input file(s) or directories.", + "Cannot find missing_file.txt. " + "Please specify valid input file(s) or directories.", ), ], ) @@ -170,7 +201,10 @@ def test_set_output_directory_bad(user_filesystem): ([], {"wavelength": 0.71073, "anode_type": "Mo"}), (["--anode-type", "Ag"], {"wavelength": 0.59, "anode_type": "Ag"}), (["--wavelength", "0.25"], {"wavelength": 0.25, "anode_type": None}), - (["--wavelength", "0.25", "--anode-type", "Ag"], {"wavelength": 0.25, "anode_type": None}), + ( + ["--wavelength", "0.25", "--anode-type", "Ag"], + {"wavelength": 0.25, "anode_type": None}, + ), ], ) def test_set_wavelength(inputs, expected): @@ -186,15 +220,20 @@ def test_set_wavelength(inputs, expected): [ ( ["--anode-type", "invalid"], - f"Anode type not recognized. Please rerun specifying an anode_type from {*known_sources, }.", + f"Anode type not recognized. " + f"Please rerun specifying an anode_type from {*known_sources, }.", ), ( ["--wavelength", "0"], - "No valid wavelength. Please rerun specifying a known anode_type or a positive wavelength.", + "No valid wavelength. " + "Please rerun specifying a known anode_type " + "or a positive wavelength.", ), ( ["--wavelength", "-1", "--anode-type", "Mo"], - "No valid wavelength. Please rerun specifying a known anode_type or a positive wavelength.", + "No valid wavelength. " + "Please rerun specifying a known anode_type " + "or a positive wavelength.", ), ], ) @@ -225,7 +264,10 @@ def test_set_xtype_bad(): cli_inputs = ["2.5", "data.xy", "--xtype", "invalid"] actual_args = get_args(cli_inputs) with pytest.raises( - ValueError, match=re.escape(f"Unknown xtype: invalid. Allowed xtypes are {*XQUANTITIES, }.") + ValueError, + match=re.escape( + f"Unknown xtype: invalid. " f"Allowed xtypes are {*XQUANTITIES, }." + ), ): actual_args = set_xtype(actual_args) @@ -252,7 +294,10 @@ def test_set_mud(user_filesystem): def test_set_mud_bad(): cli_inputs = ["2.5", "data.xy", "--z-scan-file", "invalid file"] actual_args = get_args(cli_inputs) - with pytest.raises(FileNotFoundError, match="Cannot find invalid file. Please specify a valid file path."): + with pytest.raises( + FileNotFoundError, + match="Cannot find invalid file. " "Please specify a valid file path.", + ): actual_args = set_mud(actual_args) @@ -261,8 +306,17 @@ def test_set_mud_bad(): [ ([], []), ( - ["--user-metadata", "facility=NSLS II", "beamline=28ID-2", "favorite color=blue"], - [["facility", "NSLS II"], ["beamline", "28ID-2"], ["favorite color", "blue"]], + [ + "--user-metadata", + "facility=NSLS II", + "beamline=28ID-2", + "favorite color=blue", + ], + [ + ["facility", "NSLS II"], + ["beamline", "28ID-2"], + ["favorite color", "blue"], + ], ), (["--user-metadata", "x=y=z"], [["x", "y=z"]]), ], @@ -303,7 +357,8 @@ def test_load_user_metadata(inputs, expected): ), ( ["--user-metadata", "wavelength=2"], - "wavelength is a reserved name. Please rerun using a different key name.", + "wavelength is a reserved name. " + "Please rerun using a different key name.", ), ], ) @@ -316,26 +371,51 @@ def test_load_user_metadata_bad(inputs, expected_error_msg): @pytest.mark.parametrize( "inputs, expected", - [ # Test that when cli inputs are present, they override home config, otherwise we take home config + [ # Test that when cli inputs are present, they override home config, + # otherwise we take home config ( {"username": None, "email": None, "orcid": None}, - {"username": "home_username", "email": "home@email.com", "orcid": "home_orcid"}, + { + "username": "home_username", + "email": "home@email.com", + "orcid": "home_orcid", + }, ), ( {"username": "cli_username", "email": None, "orcid": None}, - {"username": "cli_username", "email": "home@email.com", "orcid": "home_orcid"}, + { + "username": "cli_username", + "email": "home@email.com", + "orcid": "home_orcid", + }, ), ( {"username": None, "email": "cli@email.com", "orcid": None}, - {"username": "home_username", "email": "cli@email.com", "orcid": "home_orcid"}, + { + "username": "home_username", + "email": "cli@email.com", + "orcid": "home_orcid", + }, ), ( {"username": None, "email": None, "orcid": "cli_orcid"}, - {"username": "home_username", "email": "home@email.com", "orcid": "cli_orcid"}, + { + "username": "home_username", + "email": "home@email.com", + "orcid": "cli_orcid", + }, ), ( - {"username": "cli_username", "email": "cli@email.com", "orcid": "cli_orcid"}, - {"username": "cli_username", "email": "cli@email.com", "orcid": "cli_orcid"}, + { + "username": "cli_username", + "email": "cli@email.com", + "orcid": "cli_orcid", + }, + { + "username": "cli_username", + "email": "cli@email.com", + "orcid": "cli_orcid", + }, ), ], ) @@ -365,12 +445,17 @@ def test_load_user_info(monkeypatch, inputs, expected, user_filesystem): def test_load_package_info(mocker): mocker.patch( "importlib.metadata.version", - side_effect=lambda package_name: "3.3.0" if package_name == "diffpy.utils" else "1.2.3", + side_effect=lambda package_name: ( + "3.3.0" if package_name == "diffpy.utils" else "1.2.3" + ), ) cli_inputs = ["2.5", "data.xy"] actual_args = get_args(cli_inputs) actual_args = load_package_info(actual_args) - assert actual_args.package_info == {"diffpy.labpdfproc": "1.2.3", "diffpy.utils": "3.3.0"} + assert actual_args.package_info == { + "diffpy.labpdfproc": "1.2.3", + "diffpy.utils": "3.3.0", + } def test_load_metadata(mocker, user_filesystem): @@ -380,7 +465,9 @@ def test_load_metadata(mocker, user_filesystem): os.chdir(cwd) mocker.patch( "importlib.metadata.version", - side_effect=lambda package_name: "3.3.0" if package_name == "diffpy.utils" else "1.2.3", + side_effect=lambda package_name: ( + "3.3.0" if package_name == "diffpy.utils" else "1.2.3" + ), ) cli_inputs = [ "2.5", @@ -409,7 +496,10 @@ def test_load_metadata(mocker, user_filesystem): "username": "cli_username", "email": "cli@email.com", "orcid": "cli_orcid", - "package_info": {"diffpy.labpdfproc": "1.2.3", "diffpy.utils": "3.3.0"}, + "package_info": { + "diffpy.labpdfproc": "1.2.3", + "diffpy.utils": "3.3.0", + }, "z_scan_file": None, } assert actual_metadata == expected_metadata diff --git a/tests/test_version.py b/tests/test_version.py index 7e1b03b..c8bbea5 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -5,6 +5,7 @@ def test_package_version(): - """Ensure the package version is defined and not set to the initial placeholder.""" + """Ensure the package version is defined + and not set to the initial placeholder.""" assert hasattr(diffpy.labpdfproc, "__version__") assert diffpy.labpdfproc.__version__ != "0.0.0"