From 41fbe8c79cd82e1ce036fc3bb284d73a17a62b04 Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Sat, 4 Sep 2021 12:18:48 +0200 Subject: [PATCH 1/8] BUILD: start 2.0.x development branch --- azure-pipelines.yml | 6 +++--- src/pytools/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cfcf18fea..23dd21657 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,9 +1,9 @@ trigger: - - 1.2.x + - 2.0.x - release/* pr: - - 1.2.x + - 2.0.x - release/* # set the build name @@ -23,7 +23,7 @@ resources: type: github endpoint: BCG-Gamma name: BCG-Gamma/pytools - ref: 1.2.x + ref: 2.0.x variables: ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/pull/')) }}: diff --git a/src/pytools/__init__.py b/src/pytools/__init__.py index cc99b1ddf..acbffa1a3 100644 --- a/src/pytools/__init__.py +++ b/src/pytools/__init__.py @@ -1,4 +1,4 @@ """ A collection of Python extensions and tools used in BCG GAMMA's open-source libraries. """ -__version__ = "1.2.2" +__version__ = "2.0.0dev0" From e0da851a7755f4f9898ac0ad9003f9ec68169175 Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Wed, 8 Sep 2021 22:23:35 +0200 Subject: [PATCH 2/8] TEST: allow x.x.xdevx as package version --- test/test/pytools/test_package_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test/pytools/test_package_version.py b/test/test/pytools/test_package_version.py index 707e1dceb..ffde32f7d 100644 --- a/test/test/pytools/test_package_version.py +++ b/test/test/pytools/test_package_version.py @@ -22,8 +22,8 @@ def test_package_version() -> None: log.info(f"Test package version – version set to: {dev_version}") assert re.match( - r"^(\d)+\.(\d)+\.(\d)+(rc\d+)?$", dev_version - ), "pytools.__version__ is not in MAJOR.MINOR.PATCH[rcN] format." + r"^(\d)+\.(\d)+\.(\d)+((?:dev|rc)\d+)?$", dev_version + ), "pytools.__version__ is not in MAJOR.MINOR.PATCH[devN|rcN] format." releases_uri = "https://pypi.org/rss/project/gamma-pytools/releases.xml" From b52efd3162576786f1fa17472696be71450e1eec Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Thu, 9 Sep 2021 17:38:25 +0200 Subject: [PATCH 3/8] BUILD: update version to 2.0.0dev0 --- src/pytools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytools/__init__.py b/src/pytools/__init__.py index cc99b1ddf..acbffa1a3 100644 --- a/src/pytools/__init__.py +++ b/src/pytools/__init__.py @@ -1,4 +1,4 @@ """ A collection of Python extensions and tools used in BCG GAMMA's open-source libraries. """ -__version__ = "1.2.2" +__version__ = "2.0.0dev0" From f5c832d52300cfce5c317fa31f1db8c6e19a2a3f Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Thu, 9 Sep 2021 19:36:25 +0200 Subject: [PATCH 4/8] FIX: always use leaf weights for labels in dendrogram report style (#270) * FIX: always use leaf weights for labels in dendrogram report style * DOC: update release notes --- RELEASE_NOTES.rst | 17 ++++++++++++----- src/pytools/viz/dendrogram/_style.py | 11 +++++++---- .../pytools/viz/dendrogram/test_dendrogram.py | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 286800928..a4aa84ab7 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -4,12 +4,19 @@ Release Notes *pytools* 1.1 ------------- +1.1.6 +~~~~~ + +- FIX: ensure correct weight labels when rendering dendograms as plain text using the + :class:`.DendrogramReportStyle` + + 1.1.5 ~~~~~ -- FIX: fixed a rare case where :meth:`~.Expression.eq_` returned `False` for two - equivalent expressions if one of them included an :class:`~.ExpressionAlias` -- FIX: accept any type of numerical values as leaf weights of :class:`~.LinkageTree` +- FIX: fixed a rare case where :meth:`.Expression.eq_` returned `False` for two + equivalent expressions if one of them included an :class:`.ExpressionAlias` +- FIX: accept any type of numerical values as leaf weights of :class:`.LinkageTree` 1.1.4 @@ -22,7 +29,7 @@ Release Notes ~~~~~ - FIX: comparing two :class:`.InfixExpression` objects using method - :meth:`~.Expression.eq_` would erroneously yield ``True`` if both expressions + :meth:`.Expression.eq_` would erroneously yield ``True`` if both expressions had the same operator but a different number of operands, and the operands of the shorter expression were equal to the operands at the start of the longer expression @@ -62,7 +69,7 @@ Release Notes 1.0.6 ~~~~~ -- FIX: back-port 1.1 bugfix for :meth:`~.Expression.eq_` +- FIX: back-port 1.1 bugfix for :meth:`.Expression.eq_` 1.0.5 diff --git a/src/pytools/viz/dendrogram/_style.py b/src/pytools/viz/dendrogram/_style.py index 856292ce9..046b8e74b 100644 --- a/src/pytools/viz/dendrogram/_style.py +++ b/src/pytools/viz/dendrogram/_style.py @@ -19,8 +19,8 @@ # __all__ = [ - "DendrogramLineStyle", "DendrogramHeatmapStyle", + "DendrogramLineStyle", "DendrogramReportStyle", ] @@ -251,8 +251,11 @@ def draw_link_leg( # between two text lines (using an underscore symbol) is_in_between_line = round(leaf * 2) & 1 + # get the character matrix + matrix = self._char_matrix + # draw the link leg in the character matrix - self._char_matrix[ + matrix[ line_y + is_in_between_line, self._x_pos(bottom, tree_height) : self._x_pos(top, tree_height), ] = ( @@ -260,8 +263,8 @@ def draw_link_leg( ) # if we're in a leaf, we can draw the weight next to he label - if bottom == 0: - self._char_matrix[ + if bottom == 0.0 and matrix[line_y, self.label_width - 2] == " ": + matrix[ line_y, self.__weight_column : self.label_width ] = f"{weight * 100:3.0f}%" diff --git a/test/test/pytools/viz/dendrogram/test_dendrogram.py b/test/test/pytools/viz/dendrogram/test_dendrogram.py index 3a8560df8..d1bb45ed3 100644 --- a/test/test/pytools/viz/dendrogram/test_dendrogram.py +++ b/test/test/pytools/viz/dendrogram/test_dendrogram.py @@ -38,7 +38,7 @@ def linkage_tree(linkage_matrix: np.ndarray) -> LinkageTree: def test_dendrogram_drawer_text(linkage_matrix: np.ndarray) -> None: - checksum_dendrogram_report = "2d94fe5966d1fb77b4216c16e9845da6" + checksum_dendrogram_report = "32427095857f0589f68210ad4b2e8210" leaf_names = list("ABCDEFGH") leaf_weights = [(w + 1) / 36 for w in range(8)] From f87f5670e0e906c4b9a275e3c1f7c5d71955285c Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Thu, 9 Sep 2021 19:41:12 +0200 Subject: [PATCH 5/8] DOC: update release notes --- RELEASE_NOTES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 3890d5a41..d7a6ee7c4 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -4,6 +4,12 @@ Release Notes *pytools* 1.2 ------------- +1.2.3 +~~~~~ + +This is a maintenance release to catch up with *pytools* 1.1.6. + + 1.2.2 ~~~~~ From 1eea1a7944d23081375fcc8418ab6f4340ffb36b Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Thu, 9 Sep 2021 19:49:45 +0200 Subject: [PATCH 6/8] DOC: update release notes --- RELEASE_NOTES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index d7a6ee7c4..8736e045c 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -1,6 +1,13 @@ Release Notes ============= +*pytools* 2.0 +------------- + +2.0.0 +~~~~~ + + *pytools* 1.2 ------------- From d976f2c520daf5a9e55a2bdf783fbcd98833dba4 Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Fri, 10 Sep 2021 14:11:03 +0200 Subject: [PATCH 7/8] API: expect Iterable not positional args in run_jobs() and run_queues() (#269) * API: expect Iterable not positional args in run_jobs() and run_queues() * DOC: update release notes * API: rename JobQueue.collate() to .aggregate() * API: make SimpleQueue an abstract class with abstract method aggregate() * API: make JobRunner.run_queue() and JobRunner.run_queues() thread-safe * FIX: import Lock class from multiprocessing not threading module * TEST: add unit tests for parallelization package * DOC: add docstring for JobQueue.lock * API: return a list of results from JobRunner.run_queues() * FIX: fix a type hint --- RELEASE_NOTES.rst | 13 ++ .../parallelization/_parallelization.py | 134 ++++++++++-------- src/pytools/viz/_viz.py | 2 +- test/test/conftest.py | 25 ++++ test/test/pytools/test_jobs.py | 65 +++++++++ 5 files changed, 176 insertions(+), 63 deletions(-) create mode 100644 test/test/pytools/test_jobs.py diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 8736e045c..20f31d197 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -7,6 +7,19 @@ Release Notes 2.0.0 ~~~~~ +- API: revised job/queue API in :mod:`pytools.parallelization` + - method :meth:`.JobRunner.run_jobs` now expects a single iterable of :class:`.Job` + objects instead of individual jobs as positional arguments + - method :meth:`.JobRunner.run_queues` now expects a single iterable of + :class:`.JobQueue` objects instead of individual queues as positional arguments + - method :meth:`.JobRunner.run_queues` returns a list of results instead of an + iterator + - methods :meth:`.JobRunner.run_queue` and :meth:`.JobRunner.run_queues` are now + thread-safe + - rename method `collate` of class :class:`.JobQueue` to :meth:`.JobQueue.aggregate` + - :class:`.SimpleQueue` is now an abstract class, expecting subclasses to implement + method :meth:`.SimpleQueue.aggregate` + *pytools* 1.2 ------------- diff --git a/src/pytools/parallelization/_parallelization.py b/src/pytools/parallelization/_parallelization.py index 89bc6ec0c..68c4cf78f 100644 --- a/src/pytools/parallelization/_parallelization.py +++ b/src/pytools/parallelization/_parallelization.py @@ -5,12 +5,12 @@ import logging from abc import ABCMeta, abstractmethod from functools import wraps +from multiprocessing import Lock from typing import ( Any, Callable, Generic, Iterable, - Iterator, List, Optional, Sequence, @@ -32,12 +32,12 @@ # __all__ = [ - "ParallelizableMixin", "Job", "JobQueue", "JobRunner", - "SimpleQueue", "NestedQueue", + "ParallelizableMixin", + "SimpleQueue", ] # @@ -161,6 +161,13 @@ class JobQueue(Generic[T_Job_Result, T_Queue_Result], metaclass=ABCMeta): Supports :meth:`.len` to determine the number of jobs in this queue. """ + #: The lock used by class :class:`.JobRunner` to prevent parallel executions of the + #: same queue + lock: Lock + + def __init__(self) -> None: + self.lock = Lock() + @abstractmethod def jobs(self) -> Iterable[Job[T_Job_Result]]: """ @@ -179,14 +186,14 @@ def on_run(self) -> None: """ @abstractmethod - def collate(self, job_results: List[T_Job_Result]) -> T_Queue_Result: + def aggregate(self, job_results: List[T_Job_Result]) -> T_Queue_Result: """ - Called by :meth:`.JobRunner.run` to collate the results of all jobs once they + Called by :meth:`.JobRunner.run` to aggregate the results of all jobs once they have all been run. :param job_results: list of job results, ordered corresponding to the sequence of jobs generated by method :meth:`.jobs` - :return: the collated result of running the queue + :return: the aggregated result of running the queue """ pass @@ -197,7 +204,7 @@ def __len__(self) -> int: class JobRunner(ParallelizableMixin): """ - Runs job queues in parallel and collates results. + Runs job queues in parallel and aggregates results. """ @classmethod @@ -220,65 +227,73 @@ def from_parallelizable( verbose=parallelizable.verbose, ) - def run_jobs(self, *jobs: Job[T_Job_Result]) -> List[T_Job_Result]: + def run_jobs(self, jobs: Iterable[Job[T_Job_Result]]) -> List[T_Job_Result]: """ Run all given jobs in parallel. :param jobs: the jobs to run in parallel :return: the results of all jobs """ - simple_queue: JobQueue[T_Job_Result, List[T_Job_Result]] = SimpleQueue( - jobs=jobs - ) - return self.run_queue(simple_queue) + with self._parallel() as parallel: + return parallel(joblib.delayed(lambda job: job.run())(job) for job in jobs) def run_queue(self, queue: JobQueue[Any, T_Queue_Result]) -> T_Queue_Result: """ Run all jobs in the given queue, in parallel. :param queue: the queue to run - :return: the result of all jobs, collated using method :meth:`.JobQueue.collate` - :raise AssertionError: the number of results does not match the number of jobs - in the queue + :return: the result of all jobs, aggregated using method + :meth:`.JobQueue.aggregate` """ - queue.on_run() + with queue.lock: - with self._parallel() as parallel: - results: List[T_Job_Result] = parallel( - joblib.delayed(lambda job: job.run())(job) for job in queue.jobs() - ) + # notify the queue that we're about to run it + queue.on_run() - if len(results) != len(queue): - raise AssertionError( - f"Number of results ({len(results)}) does not match length of " - f"queue ({len(queue)}): check method {type(queue).__name__}.__len__()" - ) + results = self.run_jobs(queue.jobs()) + + if len(results) != len(queue): + raise AssertionError( + f"Number of results ({len(results)}) does not match length of " + f"queue ({len(queue)}): check method {type(queue).__name__}.__len__" + ) - return queue.collate(job_results=results) + return queue.aggregate(job_results=results) def run_queues( - self, *queues: JobQueue[Any, T_Queue_Result] - ) -> Iterator[T_Queue_Result]: + self, queues: Iterable[JobQueue[Any, T_Queue_Result]] + ) -> List[T_Queue_Result]: """ Run all jobs in the given queues, in parallel. - :param queues: the queues to run - :return: the result of all jobs, collated per queue using method - :meth:`.JobQueue.collate` - :raise AssertionError: the number of results does not match the total number of - jobs in the queues + :param queues: the queues to run in parallel + :return: the result of all jobs in all queues, aggregated per queue using method + :meth:`.JobQueue.aggregate` """ - for queue in queues: - queue.on_run() + queues: Sequence[JobQueue[T_Queue_Result]] = to_tuple( + queues, element_type=JobQueue, arg_name="queues" + ) - with self._parallel() as parallel: - results: List[T_Job_Result] = parallel( - joblib.delayed(lambda job: job.run())(job) - for queue in queues - for job in queue.jobs() - ) + try: + for queue in queues: + queue.lock.acquire() + + # notify the queues that we're about to run them + for queue in queues: + queue.on_run() + + with self._parallel() as parallel: + results: List[T_Job_Result] = parallel( + joblib.delayed(lambda job: job.run())(job) + for queue in queues + for job in queue.jobs() + ) + + finally: + for queue in queues: + queue.lock.release() queues_len = sum(len(queue) for queue in queues) if len(results) != queues_len: @@ -287,11 +302,12 @@ def run_queues( f"queues ({queues_len}): check method __len__() of the queue class(es)" ) - first_job = 0 - for queue in queues: - last_job = first_job + len(queue) - yield queue.collate(results[first_job:last_job]) - first_job = last_job + # split the results into a list for each queue + queue_ends = list(itertools.accumulate(len(queue) for queue in queues)) + return [ + queue.aggregate(results[first_job:last_job]) + for queue, first_job, last_job in zip(queues, [0, *queue_ends], queue_ends) + ] def _parallel(self) -> joblib.Parallel: # Generate a :class:`joblib.Parallel` instance using the parallelization @@ -300,9 +316,13 @@ def _parallel(self) -> joblib.Parallel: @inheritdoc(match="""[see superclass]""") -class SimpleQueue(JobQueue[T_Job_Result, List[T_Job_Result]], Generic[T_Job_Result]): +class SimpleQueue( + JobQueue[T_Job_Result, T_Queue_Result], + Generic[T_Job_Result, T_Queue_Result], + metaclass=ABCMeta, +): """ - A simple queue, running a given list of jobs and returning their results as a list. + A simple queue, running a given list of jobs. """ #: The jobs run by this queue. @@ -313,28 +333,18 @@ def __init__(self, jobs: Iterable[Job[T_Job_Result]]) -> None: :param jobs: jobs to be run by this queue in the given order """ super().__init__() - self._jobs = to_tuple(jobs) + self._jobs = to_tuple(jobs, element_type=Job, arg_name="jobs") def jobs(self) -> Iterable[Job[T_Job_Result]]: """[see superclass]""" return self._jobs - def collate(self, job_results: List[T_Job_Result]) -> T_Queue_Result: - """ - Return the list of job results as-is, without collating them any further. - - :param job_results: list of job results, ordered corresponding to the sequence - of jobs generated by method :meth:`.jobs` - :return: the identical list of job results - """ - return job_results - def __len__(self) -> int: return len(self._jobs) @inheritdoc(match="""[see superclass]""") -class NestedQueue(JobQueue[T_Job_Result, List[T_Job_Result]]): +class NestedQueue(JobQueue[T_Job_Result, List[T_Job_Result]], Generic[T_Job_Result]): """ Runs all jobs in a given list of compatible queues and returns their results as a flat list. @@ -356,9 +366,9 @@ def jobs(self) -> Iterable[Job[T_Job_Result]]: """[see superclass]""" return itertools.chain.from_iterable(queue.jobs() for queue in self.queues) - def collate(self, job_results: List[T_Job_Result]) -> T_Queue_Result: + def aggregate(self, job_results: List[T_Job_Result]) -> List[T_Job_Result]: """ - Return the list of job results as-is, without collating them any further. + Return the list of job results as-is, without aggregating them any further. :param job_results: list of job results, ordered corresponding to the sequence of jobs generated by method :meth:`.jobs` diff --git a/src/pytools/viz/_viz.py b/src/pytools/viz/_viz.py index 68bbef51a..49e9144c0 100644 --- a/src/pytools/viz/_viz.py +++ b/src/pytools/viz/_viz.py @@ -5,7 +5,7 @@ """ import logging from abc import ABCMeta, abstractmethod -from threading import Lock +from multiprocessing import Lock from typing import Any, Dict, Generic, Iterable, Optional, Type, TypeVar, Union, cast from ..api import AllTracker, inheritdoc diff --git a/test/test/conftest.py b/test/test/conftest.py index da0f78dd2..660ef59a1 100644 --- a/test/test/conftest.py +++ b/test/test/conftest.py @@ -1,4 +1,29 @@ import logging +from typing import List + +import pytest + +from pytools.parallelization import Job logging.basicConfig(level=logging.DEBUG) log = logging.getLogger(__name__) + + +@pytest.fixture +def jobs() -> List[Job[int]]: + # generate jobs using a class + + class TestJob(Job[int]): + def __init__(self, x: int) -> None: + self.x = x + + def run(self) -> int: + return self.x + + return [TestJob(i) for i in range(8)] + + +@pytest.fixture +def jobs_delayed() -> List[Job[int]]: + # generate jobs using class function Job.delayed + return [Job.delayed(lambda x: x + 2)(i) for i in range(4)] diff --git a/test/test/pytools/test_jobs.py b/test/test/pytools/test_jobs.py new file mode 100644 index 000000000..e622c0317 --- /dev/null +++ b/test/test/pytools/test_jobs.py @@ -0,0 +1,65 @@ +import logging +from typing import List + +from pytools.parallelization import Job, JobRunner, SimpleQueue + +log = logging.getLogger(__name__) + + +def test_jobs(jobs: List[Job[int]], jobs_delayed: List[Job[int]]) -> None: + assert JobRunner().run_jobs(jobs) == [0, 1, 2, 3, 4, 5, 6, 7] + assert JobRunner(n_jobs=1).run_jobs(jobs) == [0, 1, 2, 3, 4, 5, 6, 7] + assert JobRunner(n_jobs=-3).run_jobs(jobs) == [0, 1, 2, 3, 4, 5, 6, 7] + + assert JobRunner().run_jobs(jobs_delayed) == [2, 3, 4, 5] + assert JobRunner(n_jobs=1).run_jobs(jobs_delayed) == [2, 3, 4, 5] + assert JobRunner(n_jobs=-3).run_jobs(jobs_delayed) == [2, 3, 4, 5] + + +def test_queue(jobs: List[Job[int]], jobs_delayed: List[Job[int]]) -> None: + class PassthroughQueue(SimpleQueue[int, List[int]]): + def aggregate(self, job_results: List[int]) -> List[int]: + return job_results + + queue_1 = PassthroughQueue(jobs) + queue_2 = PassthroughQueue(jobs_delayed) + + assert JobRunner().run_queue(queue_1) == [0, 1, 2, 3, 4, 5, 6, 7] + assert JobRunner(n_jobs=1).run_queue(queue_1) == [0, 1, 2, 3, 4, 5, 6, 7] + assert JobRunner(n_jobs=-3).run_queue(queue_1) == [0, 1, 2, 3, 4, 5, 6, 7] + + assert JobRunner().run_queue(queue_2) == [2, 3, 4, 5] + assert JobRunner(n_jobs=1).run_queue(queue_2) == [2, 3, 4, 5] + assert JobRunner(n_jobs=-3).run_queue(queue_2) == [2, 3, 4, 5] + + assert list(JobRunner().run_queues([queue_1, queue_2])) == [ + [0, 1, 2, 3, 4, 5, 6, 7], + [2, 3, 4, 5], + ] + assert list(JobRunner(n_jobs=1).run_queues([queue_1, queue_2])) == [ + [0, 1, 2, 3, 4, 5, 6, 7], + [2, 3, 4, 5], + ] + assert list(JobRunner(n_jobs=-3).run_queues([queue_1, queue_2])) == [ + [0, 1, 2, 3, 4, 5, 6, 7], + [2, 3, 4, 5], + ] + + class SumQueue(SimpleQueue[int, int]): + def aggregate(self, job_results: List[int]) -> int: + return sum(job_results) + + queue_1_sum = SumQueue(jobs) + queue_2_sum = SumQueue(jobs_delayed) + + assert JobRunner().run_queue(queue_1_sum) == 28 + assert JobRunner(n_jobs=1).run_queue(queue_1_sum) == 28 + assert JobRunner(n_jobs=-3).run_queue(queue_1_sum) == 28 + + assert JobRunner().run_queue(queue_2_sum) == 14 + assert JobRunner(n_jobs=1).run_queue(queue_2_sum) == 14 + assert JobRunner(n_jobs=-3).run_queue(queue_2_sum) == 14 + + assert list(JobRunner().run_queues([queue_1_sum, queue_2_sum])) == [28, 14] + assert list(JobRunner(n_jobs=1).run_queues([queue_1_sum, queue_2_sum])) == [28, 14] + assert list(JobRunner(n_jobs=-3).run_queues([queue_1_sum, queue_2_sum])) == [28, 14] From 74ded60f3e3465450354f2e98212fe689c7ccb00 Mon Sep 17 00:00:00 2001 From: Jan Ittner Date: Fri, 10 Sep 2021 14:41:49 +0200 Subject: [PATCH 8/8] Publish development releases as pre-releases --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 23dd21657..7455a4fa5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -468,6 +468,7 @@ stages: echo "Current version: $version" echo "Detecting pre-release ('rc' in version)" prerelease=False + [[ $version == *dev* ]] && prerelease=True && echo "Development release identified" [[ $version == *rc* ]] && prerelease=True && echo "Pre-release identified" echo "##vso[task.setvariable variable=current_version]$version" echo "##vso[task.setvariable variable=is_prerelease]$prerelease"