Hello community,

here is the log from the commit of package python-guessit for openSUSE:Factory 
checked in at 2019-09-04 09:14:22
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-guessit (Old)
 and      /work/SRC/openSUSE:Factory/.python-guessit.new.7948 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-guessit"

Wed Sep  4 09:14:22 2019 rev:8 rq:727929 version:3.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-guessit/python-guessit.changes    
2019-06-12 13:16:09.940699246 +0200
+++ /work/SRC/openSUSE:Factory/.python-guessit.new.7948/python-guessit.changes  
2019-09-04 09:15:44.198934857 +0200
@@ -1,0 +2,17 @@
+Tue Sep  3 06:44:13 UTC 2019 - Luigi Baldoni <[email protected]>
+
+- Update to version 3.1.0
+  * Add python `3.8` support
+  * Use rebulk `2.*`
+  * Remove `v` from `subtitle_language` prefix in default
+    configuration
+  * Add `Variable Frame Rate` value to `other` property (VFR tag)
+  * Use episode words defined in configuration in a rebulk rule
+  * Avoid trigger of useless rules consequences
+  * Fix possible crash in weak episode removal
+  * Fix issue caused by `streaming_service` property conflicts
+  * Fix source validation when more than one pattern match
+  * Fix issue with some titles on multiple fileparts
+  * Fix issue related to website exclusion inside title
+
+-------------------------------------------------------------------

Old:
----
  guessit-3.0.4.tar.gz

New:
----
  guessit-3.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-guessit.spec ++++++
--- /var/tmp/diff_new_pack.guy7TA/_old  2019-09-04 09:15:45.050934739 +0200
+++ /var/tmp/diff_new_pack.guy7TA/_new  2019-09-04 09:15:45.054934738 +0200
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-guessit
-Version:        3.0.4
+Version:        3.1.0
 Release:        0
 Summary:        A library for guessing information from video files
 License:        LGPL-3.0-only
@@ -30,13 +30,13 @@
 BuildRequires:  %{python_module pytest-benchmark}
 BuildRequires:  %{python_module pytest-runner}
 BuildRequires:  %{python_module python-dateutil}
-BuildRequires:  %{python_module rebulk >= 0.9.0}
+BuildRequires:  %{python_module rebulk >= 2.0.0}
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-babelfish >= 0.5.5
 Requires:       python-python-dateutil
-Requires:       python-rebulk >= 0.9.0
+Requires:       python-rebulk >= 2.0.0
 BuildArch:      noarch
 %python_subpackages
 
@@ -56,7 +56,7 @@
 for i in 
{'common/comparators','common/date','common/expected','common/formatters','common/__init__','common/numeral','common/pattern','common/quantity','common/validators','common/words','__init__','markers/groups','markers/__init__','markers/path','processors'};
 do
 sed -i -e "1d" "guessit/rules/$i.py"
 done
-for i in 
{'api','backports','__init__','jsonutils','__main__','options','reutils','test/__init__','test/rules/__init__','test/rules/processors_test','test/test_api','test/test_api_unicode_literals','test/test_benchmark','test/test_main','test/test_options','test/test_yml','__version__','yamlutils'};
 do
+for i in 
{'api','backports','__init__','jsonutils','__main__','monkeypatch','options','reutils','test/__init__','test/rules/__init__','test/rules/processors_test','test/test_api','test/test_api_unicode_literals','test/test_benchmark','test/test_main','test/test_options','test/test_yml','__version__','yamlutils'};
 do
 sed -i -e "1d" "guessit/$i.py"
 done
 

++++++ guessit-3.0.4.tar.gz -> guessit-3.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/HISTORY.rst 
new/guessit-3.1.0/HISTORY.rst
--- old/guessit-3.0.4/HISTORY.rst       2019-06-04 23:30:57.000000000 +0200
+++ new/guessit-3.1.0/HISTORY.rst       2019-09-02 22:11:13.000000000 +0200
@@ -1,6 +1,22 @@
 History
 =======
 
+3.1.0 (2019-09-02)
+------------------
+
+- Add python `3.8` support
+- Use rebulk `2.*`
+- Remove `v` from `subtitle_language` prefix in default configuration
+- Add `Variable Frame Rate` value to `other` property (VFR tag)
+- Use episode words defined in configuration in a rebulk rule
+- Avoid trigger of useless rules consequences
+- Fix possible crash in weak episode removal
+- Fix issue caused by `streaming_service` property conflicts
+- Fix source validation when more than one pattern match
+- Fix issue with some titles on multiple fileparts
+- Fix issue related to website exclusion inside title
+
+
 3.0.4 (2019-06-04)
 ------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/PKG-INFO new/guessit-3.1.0/PKG-INFO
--- old/guessit-3.0.4/PKG-INFO  2019-06-04 23:30:58.000000000 +0200
+++ new/guessit-3.1.0/PKG-INFO  2019-09-02 22:11:14.000000000 +0200
@@ -1,12 +1,12 @@
 Metadata-Version: 2.1
 Name: guessit
-Version: 3.0.4
+Version: 3.1.0
 Summary: GuessIt - a library for guessing information from video filenames.
 Home-page: http://guessit.readthedocs.org/
 Author: Rémi Alvergnat
 Author-email: [email protected]
 License: LGPLv3
-Download-URL: 
https://pypi.python.org/packages/source/g/guessit/guessit-3.0.4.tar.gz
+Download-URL: 
https://pypi.python.org/packages/source/g/guessit/guessit-3.1.0.tar.gz
 Description: GuessIt
         =======
         
@@ -210,6 +210,22 @@
         History
         =======
         
+        3.1.0 (2019-09-02)
+        ------------------
+        
+        - Add python `3.8` support
+        - Use rebulk `2.*`
+        - Remove `v` from `subtitle_language` prefix in default configuration
+        - Add `Variable Frame Rate` value to `other` property (VFR tag)
+        - Use episode words defined in configuration in a rebulk rule
+        - Avoid trigger of useless rules consequences
+        - Fix possible crash in weak episode removal
+        - Fix issue caused by `streaming_service` property conflicts
+        - Fix source validation when more than one pattern match
+        - Fix issue with some titles on multiple fileparts
+        - Fix issue related to website exclusion inside title
+        
+        
         3.0.4 (2019-06-04)
         ------------------
         
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/__version__.py 
new/guessit-3.1.0/guessit/__version__.py
--- old/guessit-3.0.4/guessit/__version__.py    2019-06-04 23:30:57.000000000 
+0200
+++ new/guessit-3.1.0/guessit/__version__.py    2019-09-02 22:11:13.000000000 
+0200
@@ -4,4 +4,4 @@
 Version module
 """
 # pragma: no cover
-__version__ = '3.0.4'
+__version__ = '3.1.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/config/options.json 
new/guessit-3.1.0/guessit/config/options.json
--- old/guessit-3.0.4/guessit/config/options.json       2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/config/options.json       2019-09-02 
22:11:13.000000000 +0200
@@ -290,7 +290,6 @@
       ],
       "subtitle_prefixes": [
         "st",
-        "v",
         "vost",
         "subforced",
         "fansub",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/common/validators.py 
new/guessit-3.1.0/guessit/rules/common/validators.py
--- old/guessit-3.0.4/guessit/rules/common/validators.py        2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/common/validators.py        2019-09-02 
22:11:13.000000000 +0200
@@ -28,7 +28,7 @@
         return False
 
 
-def compose(*validators):
+def and_(*validators):
     """
     Compose validators functions
     :param validators:
@@ -49,3 +49,26 @@
                 return False
         return True
     return composed
+
+
+def or_(*validators):
+    """
+    Compose validators functions
+    :param validators:
+    :type validators:
+    :return:
+    :rtype:
+    """
+    def composed(string):
+        """
+        Composed validators function
+        :param string:
+        :type string:
+        :return:
+        :rtype:
+        """
+        for validator in validators:
+            if validator(string):
+                return True
+        return False
+    return composed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/match_processors.py 
new/guessit-3.1.0/guessit/rules/match_processors.py
--- old/guessit-3.0.4/guessit/rules/match_processors.py 1970-01-01 
01:00:00.000000000 +0100
+++ new/guessit-3.1.0/guessit/rules/match_processors.py 2019-09-02 
22:11:13.000000000 +0200
@@ -0,0 +1,20 @@
+"""
+Match processors
+"""
+from guessit.rules.common import seps
+
+
+def strip(match, chars=seps):
+    """
+    Strip given characters from match.
+
+    :param chars:
+    :param match:
+    :return:
+    """
+    while match.input_string[match.start] in chars:
+        match.start += 1
+    while match.input_string[match.end - 1] in chars:
+        match.end -= 1
+    if not match:
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/processors.py 
new/guessit-3.1.0/guessit/rules/processors.py
--- old/guessit-3.0.4/guessit/rules/processors.py       2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/processors.py       2019-09-02 
22:11:13.000000000 +0200
@@ -34,7 +34,9 @@
             for match in matches.ending(group.end - 1):
                 ending.append(match)
 
-        return starting, ending
+        if starting or ending:
+            return starting, ending
+        return False
 
     def then(self, matches, when_response, context):
         starting, ending = when_response
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/guessit-3.0.4/guessit/rules/properties/audio_codec.py 
new/guessit-3.1.0/guessit/rules/properties/audio_codec.py
--- old/guessit-3.0.4/guessit/rules/properties/audio_codec.py   2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/audio_codec.py   2019-09-02 
22:11:13.000000000 +0200
@@ -3,9 +3,8 @@
 """
 audio_codec, audio_profile and audio_channels property
 """
-from rebulk.remodule import re
-
 from rebulk import Rebulk, Rule, RemoveMatch
+from rebulk.remodule import re
 
 from ..common import dash
 from ..common.pattern import is_disabled
@@ -23,7 +22,9 @@
     :return: Created Rebulk object
     :rtype: Rebulk
     """
-    rebulk = Rebulk().regex_defaults(flags=re.IGNORECASE, 
abbreviations=[dash]).string_defaults(ignore_case=True)
+    rebulk = Rebulk()\
+        .regex_defaults(flags=re.IGNORECASE, abbreviations=[dash])\
+        .string_defaults(ignore_case=True)
 
     def audio_codec_priority(match1, match2):
         """
@@ -61,7 +62,9 @@
     rebulk.string('PCM', value='PCM')
     rebulk.string('LPCM', value='LPCM')
 
-    rebulk.defaults(name='audio_profile', disabled=lambda context: 
is_disabled(context, 'audio_profile'))
+    rebulk.defaults(clear=True,
+                    name='audio_profile',
+                    disabled=lambda context: is_disabled(context, 
'audio_profile'))
     rebulk.string('MA', value='Master Audio', tags=['audio_profile.rule', 
'DTS-HD'])
     rebulk.string('HR', 'HRA', value='High Resolution Audio', 
tags=['audio_profile.rule', 'DTS-HD'])
     rebulk.string('ES', value='Extended Surround', tags=['audio_profile.rule', 
'DTS'])
@@ -70,7 +73,9 @@
     rebulk.string('HQ', value='High Quality', tags=['audio_profile.rule', 
'Dolby Digital'])
     rebulk.string('EX', value='EX', tags=['audio_profile.rule', 'Dolby 
Digital'])
 
-    rebulk.defaults(name="audio_channels", disabled=lambda context: 
is_disabled(context, 'audio_channels'))
+    rebulk.defaults(clear=True,
+                    name="audio_channels",
+                    disabled=lambda context: is_disabled(context, 
'audio_channels'))
     rebulk.regex('7[01]', value='7.1', validator=seps_after, 
tags='weak-audio_channels')
     rebulk.regex('5[01]', value='5.1', validator=seps_after, 
tags='weak-audio_channels')
     rebulk.string('20', value='2.0', validator=seps_after, 
tags='weak-audio_channels')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/bit_rate.py 
new/guessit-3.1.0/guessit/rules/properties/bit_rate.py
--- old/guessit-3.0.4/guessit/rules/properties/bit_rate.py      2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/bit_rate.py      2019-09-02 
22:11:13.000000000 +0200
@@ -69,4 +69,6 @@
                     else:
                         to_rename.append(match)
 
-        return to_rename, to_remove
+        if to_rename or to_remove:
+            return to_rename, to_remove
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/container.py 
new/guessit-3.1.0/guessit/rules/properties/container.py
--- old/guessit-3.0.4/guessit/rules/properties/container.py     2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/container.py     2019-09-02 
22:11:13.000000000 +0200
@@ -44,7 +44,8 @@
     rebulk.regex(r'\.'+build_or_pattern(torrent)+'$', exts=torrent, 
tags=['extension', 'torrent'])
     rebulk.regex(r'\.'+build_or_pattern(nzb)+'$', exts=nzb, tags=['extension', 
'nzb'])
 
-    rebulk.defaults(name='container',
+    rebulk.defaults(clear=True,
+                    name='container',
                     validator=seps_surround,
                     formatter=lambda s: s.lower(),
                     conflict_solver=lambda match, other: match
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/guessit-3.0.4/guessit/rules/properties/episode_title.py 
new/guessit-3.1.0/guessit/rules/properties/episode_title.py
--- old/guessit-3.0.4/guessit/rules/properties/episode_title.py 2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/episode_title.py 2019-09-02 
22:11:13.000000000 +0200
@@ -10,6 +10,7 @@
 from ..common import seps, title_seps
 from ..common.formatters import cleanup
 from ..common.pattern import is_disabled
+from ..common.validators import or_
 from ..properties.title import TitleFromPosition, TitleBaseRule
 from ..properties.type import TypeProcessor
 
@@ -133,8 +134,7 @@
 
     def hole_filter(self, hole, matches):
         episode = matches.previous(hole,
-                                   lambda previous: any(name in previous.names
-                                                        for name in 
self.previous_names),
+                                   lambda previous: 
previous.named(*self.previous_names),
                                    0)
 
         crc32 = matches.named('crc32')
@@ -179,8 +179,7 @@
                                               predicate=lambda match: 'title' 
in match.tags, index=0)
             if main_title:
                 episode = matches.previous(main_title,
-                                           lambda previous: any(name in 
previous.names
-                                                                for name in 
self.previous_names),
+                                           lambda previous: 
previous.named(*self.previous_names),
                                            0)
 
                 crc32 = matches.named('crc32')
@@ -249,7 +248,7 @@
 
             if season:
                 hole = matches.holes(subdirectory.start, subdirectory.end,
-                                     ignore=lambda match: 'weak-episode' in 
match.tags,
+                                     ignore=or_(lambda match: 'weak-episode' 
in match.tags, TitleBaseRule.is_ignored),
                                      formatter=cleanup, seps=title_seps, 
predicate=lambda match: match.value,
                                      index=0)
                 if hole:
@@ -292,7 +291,8 @@
             season = (matches.range(directory.start, directory.end, lambda 
match: match.name == 'season', 0) or
                       matches.range(filename.start, filename.end, lambda 
match: match.name == 'season', 0))
             if season:
-                hole = matches.holes(directory.start, directory.end, 
ignore=lambda match: 'weak-episode' in match.tags,
+                hole = matches.holes(directory.start, directory.end,
+                                     ignore=or_(lambda match: 'weak-episode' 
in match.tags, TitleBaseRule.is_ignored),
                                      formatter=cleanup, seps=title_seps,
                                      predicate=lambda match: match.value, 
index=0)
                 if hole:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/episodes.py 
new/guessit-3.1.0/guessit/rules/properties/episodes.py
--- old/guessit-3.0.4/guessit/rules/properties/episodes.py      2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/episodes.py      2019-09-02 
22:11:13.000000000 +0200
@@ -11,12 +11,13 @@
 from rebulk.remodule import re
 from rebulk.utils import is_iterable
 
+from guessit.rules import match_processors
+from guessit.rules.common.numeral import parse_numeral, numeral
 from .title import TitleFromPosition
 from ..common import dash, alt_dash, seps, seps_no_fs
 from ..common.formatters import strip
-from ..common.numeral import numeral, parse_numeral
 from ..common.pattern import is_disabled
-from ..common.validators import compose, seps_surround, seps_before, 
int_coercable
+from ..common.validators import seps_surround, int_coercable, and_
 from ...reutils import build_or_pattern
 
 
@@ -29,17 +30,12 @@
     :return: Created Rebulk object
     :rtype: Rebulk
     """
+
     # pylint: disable=too-many-branches,too-many-statements,too-many-locals
     def is_season_episode_disabled(context):
         """Whether season and episode rules should be enabled."""
         return is_disabled(context, 'episode') or is_disabled(context, 
'season')
 
-    rebulk = 
Rebulk().regex_defaults(flags=re.IGNORECASE).string_defaults(ignore_case=True)
-    rebulk.defaults(private_names=['episodeSeparator', 'seasonSeparator', 
'episodeMarker', 'seasonMarker'])
-
-    episode_max_range = config['episode_max_range']
-    season_max_range = config['season_max_range']
-
     def episodes_season_chain_breaker(matches):
         """
         Break chains if there's more than 100 offset between two neighbor 
values.
@@ -57,8 +53,6 @@
             return True
         return False
 
-    rebulk.chain_defaults(chain_breaker=episodes_season_chain_breaker)
-
     def season_episode_conflict_solver(match, other):
         """
         Conflict solver for episode/season patterns
@@ -76,7 +70,6 @@
                 if (other.name == 'audio_channels' and 'weak-audio_channels' 
not in other.tags
                         and not match.initiator.children.named(match.name + 
'Marker')) or (
                             other.name == 'screen_size' and not 
int_coercable(other.raw)):
-
                     return match
                 if other.name in ('season', 'episode') and match.initiator != 
other.initiator:
                     if (match.initiator.name in ('weak_episode', 
'weak_duplicate')
@@ -87,21 +80,6 @@
                             return current
         return '__default__'
 
-    season_words = config['season_words']
-    episode_words = config['episode_words']
-    of_words = config['of_words']
-    all_words = config['all_words']
-    season_markers = config['season_markers']
-    season_ep_markers = config['season_ep_markers']
-    disc_markers = config['disc_markers']
-    episode_markers = config['episode_markers']
-    range_separators = config['range_separators']
-    weak_discrete_separators = list(sep for sep in seps_no_fs if sep not in 
range_separators)
-    strong_discrete_separators = config['discrete_separators']
-    discrete_separators = strong_discrete_separators + weak_discrete_separators
-
-    max_range_gap = config['max_range_gap']
-
     def ordering_validator(match):
         """
         Validator for season list. They should be in natural order to be 
validated.
@@ -135,55 +113,101 @@
                                             lambda m: m.name == property_name 
+ 'Separator')
                     separator = match.children.previous(current_match,
                                                         lambda m: m.name == 
property_name + 'Separator', 0)
-                    if separator.raw not in range_separators and separator.raw 
in weak_discrete_separators:
-                        if not 0 < current_match.value - previous_match.value 
<= max_range_gap + 1:
-                            valid = False
-                    if separator.raw in strong_discrete_separators:
-                        valid = True
-                        break
+                    if separator:
+                        if separator.raw not in range_separators and 
separator.raw in weak_discrete_separators:
+                            if not 0 < current_match.value - 
previous_match.value <= max_range_gap + 1:
+                                valid = False
+                        if separator.raw in strong_discrete_separators:
+                            valid = True
+                            break
                 previous_match = current_match
             return valid
 
         return is_consecutive('episode') and is_consecutive('season')
 
+    def validate_roman(match):
+        """
+        Validate a roman match if surrounded by separators
+        :param match:
+        :type match:
+        :return:
+        :rtype:
+        """
+        if int_coercable(match.raw):
+            return True
+        return seps_surround(match)
+
+    season_words = config['season_words']
+    episode_words = config['episode_words']
+    of_words = config['of_words']
+    all_words = config['all_words']
+    season_markers = config['season_markers']
+    season_ep_markers = config['season_ep_markers']
+    disc_markers = config['disc_markers']
+    episode_markers = config['episode_markers']
+    range_separators = config['range_separators']
+    weak_discrete_separators = list(sep for sep in seps_no_fs if sep not in 
range_separators)
+    strong_discrete_separators = config['discrete_separators']
+    discrete_separators = strong_discrete_separators + weak_discrete_separators
+    episode_max_range = config['episode_max_range']
+    season_max_range = config['season_max_range']
+    max_range_gap = config['max_range_gap']
+
+    rebulk = Rebulk() \
+        .regex_defaults(flags=re.IGNORECASE) \
+        .string_defaults(ignore_case=True) \
+        .chain_defaults(chain_breaker=episodes_season_chain_breaker) \
+        .defaults(private_names=['episodeSeparator', 'seasonSeparator', 
'episodeMarker', 'seasonMarker'],
+                  formatter={'season': int, 'episode': int, 'version': int, 
'count': int},
+                  children=True,
+                  private_parent=True,
+                  conflict_solver=season_episode_conflict_solver,
+                  abbreviations=[alt_dash])
+
     # S01E02, 01x02, S01S02S03
-    rebulk.chain(formatter={'season': int, 'episode': int},
-                 tags=['SxxExx'],
-                 abbreviations=[alt_dash],
-                 children=True,
-                 private_parent=True,
-                 validate_all=True,
-                 validator={'__parent__': ordering_validator},
-                 conflict_solver=season_episode_conflict_solver,
-                 disabled=is_season_episode_disabled) \
+    rebulk.chain(
+        tags=['SxxExx'],
+        validate_all=True,
+        validator={'__parent__': and_(seps_surround, ordering_validator)},
+        disabled=is_season_episode_disabled) \
+        .defaults(tags=['SxxExx']) \
         .regex(build_or_pattern(season_markers, name='seasonMarker') + 
r'(?P<season>\d+)@?' +
-               build_or_pattern(episode_markers + disc_markers, 
name='episodeMarker') + r'@?(?P<episode>\d+)',
-               validate_all=True,
-               validator={'__parent__': seps_before}).repeater('+') \
+               build_or_pattern(episode_markers + disc_markers, 
name='episodeMarker') + r'@?(?P<episode>\d+)')\
+        .repeater('+') \
         .regex(build_or_pattern(episode_markers + disc_markers + 
discrete_separators + range_separators,
                                 name='episodeSeparator',
                                 escape=True) +
-               r'(?P<episode>\d+)').repeater('*') \
-        .chain() \
+               r'(?P<episode>\d+)').repeater('*')
+
+    rebulk.chain(tags=['SxxExx'],
+                 validate_all=True,
+                 validator={'__parent__': and_(seps_surround, 
ordering_validator)},
+                 disabled=is_season_episode_disabled) \
+        .defaults(tags=['SxxExx']) \
         .regex(r'(?P<season>\d+)@?' +
                build_or_pattern(season_ep_markers, name='episodeMarker') +
-               r'@?(?P<episode>\d+)',
-               validate_all=True,
-               validator={'__parent__': seps_before}) \
-        .chain() \
+               r'@?(?P<episode>\d+)').repeater('+') \
+
+    rebulk.chain(tags=['SxxExx'],
+                 validate_all=True,
+                 validator={'__parent__': and_(seps_surround, 
ordering_validator)},
+                 disabled=is_season_episode_disabled) \
+        .defaults(tags=['SxxExx']) \
         .regex(r'(?P<season>\d+)@?' +
                build_or_pattern(season_ep_markers, name='episodeMarker') +
-               r'@?(?P<episode>\d+)',
-               validate_all=True,
-               validator={'__parent__': seps_before}) \
+               r'@?(?P<episode>\d+)') \
         .regex(build_or_pattern(season_ep_markers + discrete_separators + 
range_separators,
                                 name='episodeSeparator',
                                 escape=True) +
-               r'(?P<episode>\d+)').repeater('*') \
-        .chain() \
-        .regex(build_or_pattern(season_markers, name='seasonMarker') + 
r'(?P<season>\d+)',
-               validate_all=True,
-               validator={'__parent__': seps_before}) \
+               r'(?P<episode>\d+)').repeater('*')
+
+    rebulk.chain(tags=['SxxExx'],
+                 validate_all=True,
+                 validator={'__parent__': and_(seps_surround, 
ordering_validator)},
+                 disabled=is_season_episode_disabled) \
+        .defaults(tags=['SxxExx']) \
+        .regex(build_or_pattern(season_markers, name='seasonMarker') + 
r'(?P<season>\d+)') \
+        .regex('(?P<other>Extras)', name='other', value='Extras', 
tags=['no-release-group-prefix']).repeater('?') \
         .regex(build_or_pattern(season_markers + discrete_separators + 
range_separators,
                                 name='seasonSeparator',
                                 escape=True) +
@@ -191,132 +215,125 @@
 
     # episode_details property
     for episode_detail in ('Special', 'Pilot', 'Unaired', 'Final'):
-        rebulk.string(episode_detail, value=episode_detail, 
name='episode_details',
+        rebulk.string(episode_detail,
+                      private_parent=False,
+                      children=False,
+                      value=episode_detail,
+                      name='episode_details',
                       disabled=lambda context: is_disabled(context, 
'episode_details'))
 
-    def validate_roman(match):
-        """
-        Validate a roman match if surrounded by separators
-        :param match:
-        :type match:
-        :return:
-        :rtype:
-        """
-        if int_coercable(match.raw):
-            return True
-        return seps_surround(match)
-
     rebulk.defaults(private_names=['episodeSeparator', 'seasonSeparator', 
'episodeMarker', 'seasonMarker'],
-                    validate_all=True, validator={'__parent__': 
seps_surround}, children=True, private_parent=True,
+                    validate_all=True,
+                    validator={'__parent__': and_(seps_surround, 
ordering_validator)},
+                    children=True,
+                    private_parent=True,
                     conflict_solver=season_episode_conflict_solver)
 
-    rebulk.chain(abbreviations=[alt_dash],
+    rebulk.chain(validate_all=True,
+                 conflict_solver=season_episode_conflict_solver,
                  formatter={'season': parse_numeral, 'count': parse_numeral},
-                 validator={'__parent__': compose(seps_surround, 
ordering_validator),
+                 validator={'__parent__': and_(seps_surround, 
ordering_validator),
                             'season': validate_roman,
                             'count': validate_roman},
                  disabled=lambda context: context.get('type') == 'movie' or 
is_disabled(context, 'season')) \
-        .defaults(validator=None) \
+        .defaults(formatter={'season': parse_numeral, 'count': parse_numeral},
+                  validator={'season': validate_roman, 'count': 
validate_roman},
+                  conflict_solver=season_episode_conflict_solver) \
         .regex(build_or_pattern(season_words, name='seasonMarker') + 
'@?(?P<season>' + numeral + ')') \
         .regex(r'' + build_or_pattern(of_words) + '@?(?P<count>' + numeral + 
')').repeater('?') \
         .regex(r'@?' + build_or_pattern(range_separators + discrete_separators 
+ ['@'],
                                         name='seasonSeparator', escape=True) +
                r'@?(?P<season>\d+)').repeater('*')
 
+    rebulk.defaults(abbreviations=[dash])
+
     rebulk.regex(build_or_pattern(episode_words, name='episodeMarker') + 
r'-?(?P<episode>\d+)' +
                  r'(?:v(?P<version>\d+))?' +
                  r'(?:-?' + build_or_pattern(of_words) + 
r'-?(?P<count>\d+))?',  # Episode 4
-                 abbreviations=[dash], formatter={'episode': int, 'version': 
int, 'count': int},
                  disabled=lambda context: context.get('type') == 'episode' or 
is_disabled(context, 'episode'))
 
     rebulk.regex(build_or_pattern(episode_words, name='episodeMarker') + 
r'-?(?P<episode>' + numeral + ')' +
                  r'(?:v(?P<version>\d+))?' +
                  r'(?:-?' + build_or_pattern(of_words) + 
r'-?(?P<count>\d+))?',  # Episode 4
-                 abbreviations=[dash],
                  validator={'episode': validate_roman},
-                 formatter={'episode': parse_numeral, 'version': int, 'count': 
int},
+                 formatter={'episode': parse_numeral},
                  disabled=lambda context: context.get('type') != 'episode' or 
is_disabled(context, 'episode'))
 
     rebulk.regex(r'S?(?P<season>\d+)-?(?:xE|Ex|E|x)-?(?P<other>' + 
build_or_pattern(all_words) + ')',
                  tags=['SxxExx'],
-                 abbreviations=[dash],
-                 validator=None,
-                 formatter={'season': int, 'other': lambda match: 'Complete'},
+                 formatter={'other': lambda match: 'Complete'},
                  disabled=lambda context: is_disabled(context, 'season'))
 
     # 12, 13
-    rebulk.chain(tags=['weak-episode'], formatter={'episode': int, 'version': 
int},
+    rebulk.chain(tags=['weak-episode'],
                  disabled=lambda context: context.get('type') == 'movie' or 
is_disabled(context, 'episode')) \
-        .defaults(validator=None) \
+        .defaults(validator=None, tags=['weak-episode']) \
         .regex(r'(?P<episode>\d{2})') \
         .regex(r'v(?P<version>\d+)').repeater('?') \
-        .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{2})').repeater('*')
+        .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{2})', 
abbreviations=None).repeater('*')
 
     # 012, 013
-    rebulk.chain(tags=['weak-episode'], formatter={'episode': int, 'version': 
int},
+    rebulk.chain(tags=['weak-episode'],
                  disabled=lambda context: context.get('type') == 'movie' or 
is_disabled(context, 'episode')) \
-        .defaults(validator=None) \
+        .defaults(validator=None, tags=['weak-episode']) \
         .regex(r'0(?P<episode>\d{1,2})') \
         .regex(r'v(?P<version>\d+)').repeater('?') \
-        
.regex(r'(?P<episodeSeparator>[x-])0(?P<episode>\d{1,2})').repeater('*')
+        .regex(r'(?P<episodeSeparator>[x-])0(?P<episode>\d{1,2})', 
abbreviations=None).repeater('*')
 
     # 112, 113
     rebulk.chain(tags=['weak-episode'],
-                 formatter={'episode': int, 'version': int},
                  name='weak_episode',
                  disabled=lambda context: context.get('type') == 'movie' or 
is_disabled(context, 'episode')) \
-        .defaults(validator=None) \
+        .defaults(validator=None, tags=['weak-episode'], name='weak_episode') \
         .regex(r'(?P<episode>\d{3,4})') \
         .regex(r'v(?P<version>\d+)').repeater('?') \
-        .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{3,4})').repeater('*')
+        .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{3,4})', 
abbreviations=None).repeater('*')
 
     # 1, 2, 3
-    rebulk.chain(tags=['weak-episode'], formatter={'episode': int, 'version': 
int},
+    rebulk.chain(tags=['weak-episode'],
                  disabled=lambda context: context.get('type') != 'episode' or 
is_disabled(context, 'episode')) \
-        .defaults(validator=None) \
+        .defaults(validator=None, tags=['weak-episode']) \
         .regex(r'(?P<episode>\d)') \
         .regex(r'v(?P<version>\d+)').repeater('?') \
-        .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{1,2})').repeater('*')
+        .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{1,2})', 
abbreviations=None).repeater('*')
 
     # e112, e113, 1e18, 3e19
-    # TODO: Enhance rebulk for validator to be used globally 
(season_episode_validator)
-    rebulk.chain(formatter={'season': int, 'episode': int, 'version': int},
-                 disabled=lambda context: is_disabled(context, 'episode')) \
+    rebulk.chain(disabled=lambda context: is_disabled(context, 'episode')) \
         .defaults(validator=None) \
         
.regex(r'(?P<season>\d{1,2})?(?P<episodeMarker>e)(?P<episode>\d{1,4})') \
         .regex(r'v(?P<version>\d+)').repeater('?') \
-        
.regex(r'(?P<episodeSeparator>e|x|-)(?P<episode>\d{1,4})').repeater('*')
+        .regex(r'(?P<episodeSeparator>e|x|-)(?P<episode>\d{1,4})', 
abbreviations=None).repeater('*')
 
     # ep 112, ep113, ep112, ep113
-    rebulk.chain(abbreviations=[dash], formatter={'episode': int, 'version': 
int},
-                 disabled=lambda context: is_disabled(context, 'episode')) \
+    rebulk.chain(disabled=lambda context: is_disabled(context, 'episode')) \
         .defaults(validator=None) \
         .regex(r'ep-?(?P<episode>\d{1,4})') \
         .regex(r'v(?P<version>\d+)').repeater('?') \
-        
.regex(r'(?P<episodeSeparator>ep|e|x|-)(?P<episode>\d{1,4})').repeater('*')
+        .regex(r'(?P<episodeSeparator>ep|e|x|-)(?P<episode>\d{1,4})', 
abbreviations=None).repeater('*')
 
     # cap 112, cap 112_114
-    rebulk.chain(abbreviations=[dash],
-                 tags=['see-pattern'],
-                 formatter={'season': int, 'episode': int},
+    rebulk.chain(tags=['see-pattern'],
                  disabled=is_season_episode_disabled) \
-        .defaults(validator=None) \
+        .defaults(validator=None, tags=['see-pattern']) \
         
.regex(r'(?P<seasonMarker>cap)-?(?P<season>\d{1,2})(?P<episode>\d{2})') \
         
.regex(r'(?P<episodeSeparator>-)(?P<season>\d{1,2})(?P<episode>\d{2})').repeater('?')
 
     # 102, 0102
     rebulk.chain(tags=['weak-episode', 'weak-duplicate'],
-                 formatter={'season': int, 'episode': int, 'version': int},
                  name='weak_duplicate',
                  conflict_solver=season_episode_conflict_solver,
                  disabled=lambda context: 
(context.get('episode_prefer_number', False) or
                                            context.get('type') == 'movie') or 
is_season_episode_disabled(context)) \
-        .defaults(validator=None) \
+        .defaults(tags=['weak-episode', 'weak-duplicate'],
+                  name='weak_duplicate',
+                  validator=None,
+                  conflict_solver=season_episode_conflict_solver) \
         .regex(r'(?P<season>\d{1,2})(?P<episode>\d{2})') \
         .regex(r'v(?P<version>\d+)').repeater('?') \
-        .regex(r'(?P<episodeSeparator>x|-)(?P<episode>\d{2})').repeater('*')
+        .regex(r'(?P<episodeSeparator>x|-)(?P<episode>\d{2})', 
abbreviations=None).repeater('*')
 
-    rebulk.regex(r'v(?P<version>\d+)', children=True, private_parent=True, 
formatter=int,
+    rebulk.regex(r'v(?P<version>\d+)',
+                 formatter=int,
                  disabled=lambda context: is_disabled(context, 'version'))
 
     rebulk.defaults(private_names=['episodeSeparator', 'seasonSeparator'])
@@ -325,18 +342,23 @@
     # detached of X count (season/episode)
     rebulk.regex(r'(?P<episode>\d+)-?' + build_or_pattern(of_words) +
                  r'-?(?P<count>\d+)-?' + build_or_pattern(episode_words) + '?',
-                 abbreviations=[dash], children=True, private_parent=True, 
formatter=int,
+                 formatter=int,
+                 pre_match_processor=match_processors.strip,
                  disabled=lambda context: is_disabled(context, 'episode'))
 
-    rebulk.regex(r'Minisodes?', name='episode_format', value="Minisode",
+    rebulk.regex(r'Minisodes?',
+                 children=False,
+                 private_parent=False,
+                 name='episode_format',
+                 value="Minisode",
                  disabled=lambda context: is_disabled(context, 
'episode_format'))
 
     rebulk.rules(WeakConflictSolver, RemoveInvalidSeason, RemoveInvalidEpisode,
                  SeePatternRange(range_separators + ['_']),
                  EpisodeNumberSeparatorRange(range_separators),
-                 SeasonSeparatorRange(range_separators), RemoveWeakIfMovie, 
RemoveWeakIfSxxExx,
-                 RemoveWeakDuplicate, EpisodeDetailValidator, 
RemoveDetachedEpisodeNumber, VersionValidator,
-                 RemoveWeak, RenameToAbsoluteEpisode, CountValidator, 
EpisodeSingleDigitValidator, RenameToDiscMatch)
+                 SeasonSeparatorRange(range_separators), RemoveWeakIfMovie, 
RemoveWeakIfSxxExx, RemoveWeakDuplicate,
+                 EpisodeDetailValidator, RemoveDetachedEpisodeNumber, 
VersionValidator, RemoveWeak(episode_words),
+                 RenameToAbsoluteEpisode, CountValidator, 
EpisodeSingleDigitValidator, RenameToDiscMatch)
 
     return rebulk
 
@@ -416,7 +438,9 @@
                 if to_append:
                     to_remove.extend(weak_dup_matches)
 
-        return to_remove, to_append
+        if to_remove or to_append:
+            return to_remove, to_append
+        return False
 
 
 class CountValidator(Rule):
@@ -442,7 +466,9 @@
                     season_count.append(count)
             else:
                 to_remove.append(count)
-        return to_remove, episode_count, season_count
+        if to_remove or episode_count or season_count:
+            return to_remove, episode_count, season_count
+        return False
 
 
 class SeePatternRange(Rule):
@@ -477,7 +503,9 @@
 
             to_remove.append(separator)
 
-        return to_remove, to_append
+        if to_remove or to_append:
+            return to_remove, to_append
+        return False
 
 
 class AbstractSeparatorRange(Rule):
@@ -533,7 +561,9 @@
 
             previous_match = next_match
 
-        return to_remove, to_append
+        if to_remove or to_append:
+            return to_remove, to_append
+        return False
 
 
 class RenameToAbsoluteEpisode(Rule):
@@ -631,29 +661,39 @@
     priority = 16
     consequence = RemoveMatch, AppendMatch
 
+    def __init__(self, episode_words):
+        super(RemoveWeak, self).__init__()
+        self.episode_words = episode_words
+
     def when(self, matches, context):
         to_remove = []
         to_append = []
         for filepart in matches.markers.named('path'):
             weaks = matches.range(filepart.start, filepart.end, 
predicate=lambda m: 'weak-episode' in m.tags)
             if weaks:
-                previous = matches.previous(weaks[0], predicate=lambda m: 
m.name in (
+                weak = weaks[0]
+                previous = matches.previous(weak, predicate=lambda m: m.name 
in (
                     'audio_codec', 'screen_size', 'streaming_service', 
'source', 'video_profile',
                     'audio_channels', 'audio_profile'), index=0)
                 if previous and not matches.holes(
-                        previous.end, weaks[0].start, predicate=lambda m: 
m.raw.strip(seps)):
-                    if previous.raw.lower() == 'ep':
-                        episode = copy.copy(weaks[0])
-                        episode.name = 'episode'
-                        episode.value = int(weaks[0].value)
-                        episode.start = previous.start
-                        episode.private = False
-                        episode.tags = []
-
-                        to_append.append(episode)
+                        previous.end, weak.start, predicate=lambda m: 
m.raw.strip(seps)):
+                    if previous.raw.lower() in self.episode_words:
+                        try:
+                            episode = copy.copy(weak)
+                            episode.name = 'episode'
+                            episode.value = int(weak.value)
+                            episode.start = previous.start
+                            episode.private = False
+                            episode.tags = []
+
+                            to_append.append(episode)
+                        except ValueError:
+                            pass
 
                     to_remove.extend(weaks)
-        return to_remove, to_append
+        if to_remove or to_append:
+            return to_remove, to_append
+        return False
 
 
 class RemoveWeakIfSxxExx(Rule):
@@ -867,4 +907,6 @@
             markers.append(marker)
             discs.extend(sorted(marker.initiator.children.named('episode'), 
key=lambda m: m.value))
 
-        return discs, markers, to_remove
+        if discs or markers or to_remove:
+            return discs, markers, to_remove
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/language.py 
new/guessit-3.1.0/guessit/rules/properties/language.py
--- old/guessit-3.0.4/guessit/rules/properties/language.py      2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/language.py      2019-09-02 
22:11:13.000000000 +0200
@@ -390,7 +390,9 @@
                 to_remove.extend(matches.conflicting(lang))
                 if prefix in to_remove:
                     to_remove.remove(prefix)
-        return to_rename, to_remove
+        if to_rename or to_remove:
+            return to_rename, to_remove
+        return False
 
     def then(self, matches, when_response, context):
         to_rename, to_remove = when_response
@@ -427,7 +429,9 @@
                 to_append.append(lang)
                 if suffix in to_remove:
                     to_remove.remove(suffix)
-        return to_append, to_remove
+        if to_append or to_remove:
+            return to_append, to_remove
+        return False
 
     def then(self, matches, when_response, context):
         to_rename, to_remove = when_response
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/other.py 
new/guessit-3.1.0/guessit/rules/properties/other.py
--- old/guessit-3.0.4/guessit/rules/properties/other.py 2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/other.py 2019-09-02 
22:11:13.000000000 +0200
@@ -11,7 +11,7 @@
 from ..common import dash
 from ..common import seps
 from ..common.pattern import is_disabled
-from ..common.validators import seps_after, seps_before, seps_surround, compose
+from ..common.validators import seps_after, seps_before, seps_surround, and_
 from ...reutils import build_or_pattern
 from ...rules.common.formatters import raw_cleanup
 
@@ -77,11 +77,12 @@
                  private_names=['completeArticle', 'completeWordsBefore', 
'completeWordsAfter'],
                  value={'other': 'Complete'},
                  tags=['release-group-prefix'],
-                 validator={'__parent__': compose(seps_surround, 
validate_complete)})
+                 validator={'__parent__': and_(seps_surround, 
validate_complete)})
     rebulk.string('R5', value='Region 5')
     rebulk.string('RC', value='Region C')
     rebulk.regex('Pre-?Air', value='Preair')
-    rebulk.regex('(?:PS-?)?Vita', value='PS Vita')
+    rebulk.regex('(?:PS-?)Vita', value='PS Vita')
+    rebulk.regex('Vita', value='PS Vita', tags='has-neighbor')
     rebulk.regex('(HD)(?P<another>Rip)', value={'other': 'HD', 'another': 
'Rip'},
                  private_parent=True, children=True, validator={'__parent__': 
seps_surround}, validate_all=True)
 
@@ -96,6 +97,7 @@
     rebulk.string('mHD', 'HDLight', value='Micro HD')
     rebulk.string('LDTV', value='Low Definition')
     rebulk.string('HFR', value='High Frame Rate')
+    rebulk.string('VFR', value='Variable Frame Rate')
     rebulk.string('HD', value='HD', validator=None,
                   tags=['streaming_service.prefix', 
'streaming_service.suffix'])
     rebulk.regex('Full-?HD', 'FHD', value='Full HD', validator=None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/part.py 
new/guessit-3.1.0/guessit/rules/properties/part.py
--- old/guessit-3.0.4/guessit/rules/properties/part.py  2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/part.py  2019-09-02 
22:11:13.000000000 +0200
@@ -8,7 +8,7 @@
 from rebulk import Rebulk
 from ..common import dash
 from ..common.pattern import is_disabled
-from ..common.validators import seps_surround, int_coercable, compose
+from ..common.validators import seps_surround, int_coercable, and_
 from ..common.numeral import numeral, parse_numeral
 from ...reutils import build_or_pattern
 
@@ -41,6 +41,6 @@
 
     rebulk.regex(build_or_pattern(prefixes) + r'-?(?P<part>' + numeral + r')',
                  prefixes=prefixes, validate_all=True, private_parent=True, 
children=True, formatter=parse_numeral,
-                 validator={'part': compose(validate_roman, lambda m: 0 < 
m.value < 100)})
+                 validator={'part': and_(validate_roman, lambda m: 0 < m.value 
< 100)})
 
     return rebulk
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/guessit-3.0.4/guessit/rules/properties/release_group.py 
new/guessit-3.1.0/guessit/rules/properties/release_group.py
--- old/guessit-3.0.4/guessit/rules/properties/release_group.py 2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/release_group.py 2019-09-02 
22:11:13.000000000 +0200
@@ -9,8 +9,8 @@
 from rebulk.match import Match
 
 from ..common import seps
-from ..common.expected import build_expected_function
 from ..common.comparators import marker_sorted
+from ..common.expected import build_expected_function
 from ..common.formatters import cleanup
 from ..common.pattern import is_disabled
 from ..common.validators import int_coercable, seps_surround
@@ -72,7 +72,9 @@
                          'audio_channels', 'screen_size', 'other', 
'container', 'language', 'subtitle_language',
                          'subtitle_language.suffix', 
'subtitle_language.prefix', 'language.suffix')
 
-_scene_previous_tags = ('release-group-prefix', )
+_scene_previous_tags = ('release-group-prefix',)
+
+_scene_no_previous_tags = ('no-release-group-prefix',)
 
 
 class DashSeparatedReleaseGroup(Rule):
@@ -193,7 +195,8 @@
 
                 if releasegroup.value:
                     to_append.append(releasegroup)
-                return to_remove, to_append
+                if to_remove or to_append:
+                    return to_remove, to_append
 
 
 class SceneReleaseGroup(Rule):
@@ -212,6 +215,17 @@
         super(SceneReleaseGroup, self).__init__()
         self.value_formatter = value_formatter
 
+    @staticmethod
+    def is_previous_match(match):
+        """
+        Check if match can precede release_group
+
+        :param match:
+        :return:
+        """
+        return not match.tagged(*_scene_no_previous_tags) if match.name in 
_scene_previous_names else \
+            match.tagged(*_scene_previous_tags)
+
     def when(self, matches, context):  # pylint:disable=too-many-locals
         # If a release_group is found before, ignore this kind of 
release_group rule.
 
@@ -253,13 +267,12 @@
 
                     if match.start < filepart.start:
                         return False
-                    return not match.private or match.name in 
_scene_previous_names
+                    return not match.private or self.is_previous_match(match)
 
                 previous_match = matches.previous(last_hole,
                                                   previous_match_filter,
                                                   index=0)
-                if previous_match and (previous_match.name in 
_scene_previous_names or
-                                       any(tag in previous_match.tags for tag 
in _scene_previous_tags)) and \
+                if previous_match and (self.is_previous_match(previous_match)) 
and \
                         not 
matches.input_string[previous_match.end:last_hole.start].strip(seps) \
                         and not int_coercable(last_hole.value.strip(seps)):
 
@@ -300,11 +313,11 @@
 
         # If a release_group is found before, ignore this kind of 
release_group rule.
         if matches.named('release_group'):
-            return to_remove, to_append
+            return False
 
         if not matches.named('episode') and not matches.named('season') and 
matches.named('release_group'):
             # This doesn't seems to be an anime, and we already found another 
release_group.
-            return to_remove, to_append
+            return False
 
         for filepart in marker_sorted(matches.markers.named('path'), matches):
 
@@ -328,4 +341,7 @@
                 to_append.append(group)
                 to_remove.extend(matches.range(empty_group.start, 
empty_group.end,
                                                lambda m: 'weak-language' in 
m.tags))
-        return to_remove, to_append
+
+        if to_remove or to_append:
+            return to_remove, to_append
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/source.py 
new/guessit-3.1.0/guessit/rules/properties/source.py
--- old/guessit-3.0.4/guessit/rules/properties/source.py        2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/source.py        2019-09-02 
22:11:13.000000000 +0200
@@ -12,7 +12,7 @@
 from .audio_codec import HqConflictRule
 from ..common import dash, seps
 from ..common.pattern import is_disabled
-from ..common.validators import seps_before, seps_after
+from ..common.validators import seps_before, seps_after, or_
 
 
 def source(config):  # pylint:disable=unused-argument
@@ -26,7 +26,10 @@
     """
     rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'source'))
     rebulk = rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash], 
private_parent=True, children=True)
-    rebulk.defaults(name='source', tags=['video-codec-prefix', 
'streaming_service.suffix'])
+    rebulk = rebulk.defaults(name='source',
+                             tags=['video-codec-prefix', 
'streaming_service.suffix'],
+                             validate_all=True,
+                             validator={'__parent__': or_(seps_before, 
seps_after)})
 
     rip_prefix = '(?P<other>Rip)-?'
     rip_suffix = '-?(?P<other>Rip)'
@@ -42,7 +45,7 @@
 
     def demote_other(match, other):  # pylint: disable=unused-argument
         """Default conflict solver with 'other' property."""
-        return other if other.name == 'other' else '__default__'
+        return other if other.name == 'other' or other.name == 'release_group' 
else '__default__'
 
     rebulk.regex(*build_source_pattern('VHS', suffix=rip_optional_suffix),
                  value={'source': 'VHS', 'other': 'Rip'})
@@ -119,7 +122,7 @@
     rebulk.regex(*build_source_pattern('DSR?', 'SAT', suffix=rip_suffix),
                  value={'source': 'Satellite', 'other': 'Rip'})
 
-    rebulk.rules(ValidateSource, UltraHdBlurayRule)
+    rebulk.rules(ValidateSourcePrefixSuffix, ValidateWeakSource, 
UltraHdBlurayRule)
 
     return rebulk
 
@@ -171,10 +174,12 @@
                 to_remove.append(match)
                 to_append.append(new_source)
 
-        return to_remove, to_append
+        if to_remove or to_append:
+            return to_remove, to_append
+        return False
 
 
-class ValidateSource(Rule):
+class ValidateSourcePrefixSuffix(Rule):
     """
     Validate source with source prefix, source suffix.
     """
@@ -201,6 +206,21 @@
                     ret.append(match)
                     continue
 
+        return ret
+
+
+class ValidateWeakSource(Rule):
+    """
+    Validate weak source
+    """
+    dependency = [ValidateSourcePrefixSuffix]
+    priority = 64
+    consequence = RemoveMatch
+
+    def when(self, matches, context):
+        ret = []
+        for filepart in matches.markers.named('path'):
+            for match in matches.range(filepart.start, filepart.end, 
predicate=lambda m: m.name == 'source'):
                 # if there are more than 1 source in this filepart, just 
before the year and with holes for the title
                 # most likely the source is part of the title
                 if 'weak.source' in match.tags \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/guessit-3.0.4/guessit/rules/properties/streaming_service.py 
new/guessit-3.1.0/guessit/rules/properties/streaming_service.py
--- old/guessit-3.0.4/guessit/rules/properties/streaming_service.py     
2019-06-04 23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/streaming_service.py     
2019-09-02 22:11:13.000000000 +0200
@@ -41,7 +41,7 @@
 class ValidateStreamingService(Rule):
     """Validate streaming service matches."""
 
-    priority = 32
+    priority = 128
     consequence = RemoveMatch
 
     def when(self, matches, context):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/title.py 
new/guessit-3.1.0/guessit/rules/properties/title.py
--- old/guessit-3.0.4/guessit/rules/properties/title.py 2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/title.py 2019-09-02 
22:11:13.000000000 +0200
@@ -104,7 +104,8 @@
 
         return cropped_holes
 
-    def is_ignored(self, match):
+    @staticmethod
+    def is_ignored(match):
         """
         Ignore matches when scanning for title (hole).
 
@@ -251,7 +252,7 @@
         to_remove = []
 
         if matches.named(self.match_name, lambda match: 'expected' in 
match.tags):
-            return ret, to_remove
+            return False
 
         fileparts = [filepart for filepart in 
list(marker_sorted(matches.markers.named('path'), matches))
                      if not self.filepart_filter or 
self.filepart_filter(filepart, matches)]
@@ -284,7 +285,9 @@
                 ret.extend(titles)
                 to_remove.extend(to_remove_c)
 
-        return ret, to_remove
+        if ret or to_remove:
+            return ret, to_remove
+        return False
 
 
 class TitleFromPosition(TitleBaseRule):
@@ -341,4 +344,6 @@
         for title_match in titles:
             if title_match.value not in title_values:
                 to_remove.append(title_match)
-        return to_remove, to_tag
+        if to_remove or to_tag:
+            return to_remove, to_tag
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/guessit-3.0.4/guessit/rules/properties/video_codec.py 
new/guessit-3.1.0/guessit/rules/properties/video_codec.py
--- old/guessit-3.0.4/guessit/rules/properties/video_codec.py   2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/video_codec.py   2019-09-02 
22:11:13.000000000 +0200
@@ -3,9 +3,8 @@
 """
 video_codec and video_profile property
 """
-from rebulk.remodule import re
-
 from rebulk import Rebulk, Rule, RemoveMatch
+from rebulk.remodule import re
 
 from ..common import dash
 from ..common.pattern import is_disabled
@@ -43,7 +42,8 @@
 
     # http://blog.mediacoderhq.com/h264-profiles-and-levels/
     # https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC
-    rebulk.defaults(name="video_profile",
+    rebulk.defaults(clear=True,
+                    name="video_profile",
                     validator=seps_surround,
                     disabled=lambda context: is_disabled(context, 
'video_profile'))
 
@@ -66,7 +66,8 @@
     rebulk.string('DXVA', value='DXVA', name='video_api',
                   disabled=lambda context: is_disabled(context, 'video_api'))
 
-    rebulk.defaults(name='color_depth',
+    rebulk.defaults(clear=True,
+                    name='color_depth',
                     validator=seps_surround,
                     disabled=lambda context: is_disabled(context, 
'color_depth'))
     rebulk.regex('12.?bits?', value='12-bit')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/rules/properties/website.py 
new/guessit-3.1.0/guessit/rules/properties/website.py
--- old/guessit-3.0.4/guessit/rules/properties/website.py       2019-06-04 
23:30:57.000000000 +0200
+++ new/guessit-3.1.0/guessit/rules/properties/website.py       2019-09-02 
22:11:13.000000000 +0200
@@ -67,7 +67,7 @@
             """
             Validator for next website matches
             """
-            return any(name in ['season', 'episode', 'year'] for name in 
match.names)
+            return match.named('season', 'episode', 'year')
 
         def when(self, matches, context):
             to_remove = []
@@ -80,7 +80,9 @@
                 if not safe:
                     suffix = matches.next(website_match, 
PreferTitleOverWebsite.valid_followers, 0)
                     if suffix:
-                        to_remove.append(website_match)
+                        group = matches.markers.at_match(website_match, lambda 
marker: marker.name == 'group', 0)
+                        if not group:
+                            to_remove.append(website_match)
             return to_remove
 
     rebulk.rules(PreferTitleOverWebsite, ValidateWebsitePrefix)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/test/episodes.yml 
new/guessit-3.1.0/guessit/test/episodes.yml
--- old/guessit-3.0.4/guessit/test/episodes.yml 2019-06-04 23:30:57.000000000 
+0200
+++ new/guessit-3.1.0/guessit/test/episodes.yml 2019-09-02 22:11:13.000000000 
+0200
@@ -201,9 +201,9 @@
 ? Series/My Name Is Earl/My.Name.Is.Earl.S01Extras.-.Bad.Karma.DVDRip.XviD.avi
 : title: My Name Is Earl
   season: 1
-  episode_title: Extras - Bad Karma
+  episode_title: Bad Karma
   source: DVD
-  other: Rip
+  other: [Extras, Rip]
   video_codec: Xvid
 
 ? series/Freaks And Geeks/Season 1/Episode 4 - Kim Kelly Is My 
Friend-eng(1).srt
@@ -3029,7 +3029,7 @@
   title: Show Name
   episode: [493, 494, 495, 496, 497, 498, 500, 501, 502, 503, 504, 505, 506, 
507]
   screen_size: 720p
-  subtitle_language: fr
+  other: Variable Frame Rate
   video_codec: H.264
   audio_codec: AAC
   type: episode
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/test/test_yml.py 
new/guessit-3.1.0/guessit/test/test_yml.py
--- old/guessit-3.0.4/guessit/test/test_yml.py  2019-06-04 23:30:57.000000000 
+0200
+++ new/guessit-3.1.0/guessit/test/test_yml.py  2019-09-02 22:11:13.000000000 
+0200
@@ -7,7 +7,6 @@
 from io import open  # pylint: disable=redefined-builtin
 
 import babelfish
-import pytest  # pylint:disable=wrong-import-order
 import six  # pylint:disable=wrong-import-order
 import yaml  # pylint:disable=wrong-import-order
 from rebulk.remodule import re
@@ -21,13 +20,6 @@
 
 __location__ = os.path.realpath(os.path.join(os.getcwd(), 
os.path.dirname(__file__)))
 
-filename_predicate = None
-string_predicate = None
-
-
-# filename_predicate = lambda filename: 'episode_title' in filename
-# string_predicate = lambda string: '-DVD.BlablaBla.Fix.Blablabla.XVID' in 
string
-
 
 class EntryResult(object):
     def __init__(self, string, negates=False):
@@ -134,7 +126,49 @@
 
     options_re = re.compile(r'^([ +-]+)(.*)')
 
-    files, ids = files_and_ids(filename_predicate)
+    def _get_unique_id(self, collection, base_id):
+        ret = base_id
+        i = 2
+        while ret in collection:
+            suffix = "-" + str(i)
+            ret = base_id + suffix
+            i += 1
+        return ret
+
+    def pytest_generate_tests(self, metafunc):
+        if 'yml_test_case' in metafunc.fixturenames:
+            entries = []
+            entry_ids = []
+            entry_set = set()
+
+            for filename, _ in zip(*files_and_ids()):
+                with open(os.path.join(__location__, filename), 'r', 
encoding='utf-8') as infile:
+                    data = yaml.load(infile, OrderedDictYAMLLoader)
+
+                last_expected = None
+                for string, expected in reversed(list(data.items())):
+                    if expected is None:
+                        data[string] = last_expected
+                    else:
+                        last_expected = expected
+
+                default = None
+                try:
+                    default = data['__default__']
+                    del data['__default__']
+                except KeyError:
+                    pass
+
+                for string, expected in data.items():
+                    TestYml.set_default(expected, default)
+                    string = TestYml.fix_encoding(string, expected)
+
+                    entries.append((filename, string, expected))
+                    unique_id = self._get_unique_id(entry_set, '[' + filename 
+ '] ' + str(string))
+                    entry_set.add(unique_id)
+                    entry_ids.append(unique_id)
+
+            metafunc.parametrize('yml_test_case', entries, ids=entry_ids)
 
     @staticmethod
     def set_default(expected, default):
@@ -143,34 +177,8 @@
                 if k not in expected:
                     expected[k] = v
 
-    @pytest.mark.parametrize('filename', files, ids=ids)
-    def test(self, filename, caplog):
-        caplog.set_level(logging.INFO)
-        with open(os.path.join(__location__, filename), 'r', encoding='utf-8') 
as infile:
-            data = yaml.load(infile, OrderedDictYAMLLoader)
-        entries = Results()
-
-        last_expected = None
-        for string, expected in reversed(list(data.items())):
-            if expected is None:
-                data[string] = last_expected
-            else:
-                last_expected = expected
-
-        default = None
-        try:
-            default = data['__default__']
-            del data['__default__']
-        except KeyError:
-            pass
-
-        for string, expected in data.items():
-            TestYml.set_default(expected, default)
-            entry = self.check_data(filename, string, expected)
-            entries.append(entry)
-        entries.assert_ok()
-
-    def check_data(self, filename, string, expected):
+    @classmethod
+    def fix_encoding(cls, string, expected):
         if six.PY2:
             if isinstance(string, six.text_type):
                 string = string.encode('utf-8')
@@ -183,16 +191,23 @@
                 expected[k] = v
         if not isinstance(string, str):
             string = str(string)
-        if not string_predicate or string_predicate(string):  # pylint: 
disable=not-callable
-            entry = self.check(string, expected)
-            if entry.ok:
-                logger.debug('[%s] %s', filename, entry)
-            elif entry.warning:
-                logger.warning('[%s] %s', filename, entry)
-            elif entry.error:
-                logger.error('[%s] %s', filename, entry)
-                for line in entry.details:
-                    logger.error('[%s] %s', filename, ' ' * 4 + line)
+        return string
+
+    def test_entry(self, yml_test_case):
+        filename, string, expected = yml_test_case
+        result = self.check_data(filename, string, expected)
+        assert not result.error
+
+    def check_data(self, filename, string, expected):
+        entry = self.check(string, expected)
+        if entry.ok:
+            logger.debug('[%s] %s', filename, entry)
+        elif entry.warning:
+            logger.warning('[%s] %s', filename, entry)
+        elif entry.error:
+            logger.error('[%s] %s', filename, entry)
+            for line in entry.details:
+                logger.error('[%s] %s', filename, ' ' * 4 + line)
         return entry
 
     def check(self, string, expected):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit/test/various.yml 
new/guessit-3.1.0/guessit/test/various.yml
--- old/guessit-3.0.4/guessit/test/various.yml  2019-06-04 23:30:57.000000000 
+0200
+++ new/guessit-3.1.0/guessit/test/various.yml  2019-09-02 22:11:13.000000000 
+0200
@@ -1114,3 +1114,86 @@
   video_codec: H.264
   release_group: W4F
   type: episode
+
+? NOS4A2.S01E01.The.Shorter.Way.REPACK.720p.AMZN.WEB-DL.DDP5.1.H.264-NTG.mkv
+: title: NOS4A2
+  season: 1
+  episode: 1
+  episode_title: The Shorter Way
+  other: Proper
+  proper_count: 1
+  screen_size: 720p
+  streaming_service: Amazon Prime
+  source: Web
+  audio_codec: Dolby Digital Plus
+  audio_channels: '5.1'
+  video_codec: H.264
+  release_group: NTG
+  container: mkv
+  type: episode
+
+? Star Trek DS9 Ep 2x03 The Siege (Part III)
+: title: Star Trek DS9
+  season: 2
+  episode: 3
+  episode_title: The Siege
+  part: 3
+  type: episode
+
+? The.Red.Line.S01E01
+: title: The Red Line
+  season: 1
+  episode: 1
+  type: episode
+
+? Show.S01E01.WEB.x264-METCON.mkv
+: title: Show
+  season: 1
+  episode: 1
+  source: Web
+  video_codec: H.264
+  release_group: METCON
+  container: mkv
+  type: episode
+
+? Show.S01E01.WEB.x264-TCMEON.mkv
+: title: Show
+  season: 1
+  episode: 1
+  source: Web
+  video_codec: H.264
+  release_group: TCMEON
+  container: mkv
+  type: episode
+
+? Show.S01E01.WEB.x264-MEONTC.mkv
+: title: Show
+  season: 1
+  episode: 1
+  source: Web
+  video_codec: H.264
+  release_group: MEONTC
+  container: mkv
+  type: episode
+
+? 
'[TorrentCouch.com].Westworld.S02.Complete.720p.WEB-DL.x264.[MP4].[5.3GB].[Season.2.Full]/[TorrentCouch.com].Westworld.S02E03.720p.WEB-DL.x264.mp4'
+: website: TorrentCouch.com
+  title: Westworld
+  season: 2
+  other: Complete
+  screen_size: 720p
+  source: Web
+  video_codec: H.264
+  container: mp4
+  size: 5.3GB
+  episode: 3
+  type: episode
+
+? Vita.&.Virginia.2018.720p.H.264.YTS.LT.mp4
+: title: Vita & Virginia
+  year: 2018
+  screen_size: 720p
+  video_codec: H.264
+  release_group: YTS.LT
+  container: mp4
+  type: movie
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit.egg-info/PKG-INFO 
new/guessit-3.1.0/guessit.egg-info/PKG-INFO
--- old/guessit-3.0.4/guessit.egg-info/PKG-INFO 2019-06-04 23:30:58.000000000 
+0200
+++ new/guessit-3.1.0/guessit.egg-info/PKG-INFO 2019-09-02 22:11:14.000000000 
+0200
@@ -1,12 +1,12 @@
 Metadata-Version: 2.1
 Name: guessit
-Version: 3.0.4
+Version: 3.1.0
 Summary: GuessIt - a library for guessing information from video filenames.
 Home-page: http://guessit.readthedocs.org/
 Author: Rémi Alvergnat
 Author-email: [email protected]
 License: LGPLv3
-Download-URL: 
https://pypi.python.org/packages/source/g/guessit/guessit-3.0.4.tar.gz
+Download-URL: 
https://pypi.python.org/packages/source/g/guessit/guessit-3.1.0.tar.gz
 Description: GuessIt
         =======
         
@@ -210,6 +210,22 @@
         History
         =======
         
+        3.1.0 (2019-09-02)
+        ------------------
+        
+        - Add python `3.8` support
+        - Use rebulk `2.*`
+        - Remove `v` from `subtitle_language` prefix in default configuration
+        - Add `Variable Frame Rate` value to `other` property (VFR tag)
+        - Use episode words defined in configuration in a rebulk rule
+        - Avoid trigger of useless rules consequences
+        - Fix possible crash in weak episode removal
+        - Fix issue caused by `streaming_service` property conflicts
+        - Fix source validation when more than one pattern match
+        - Fix issue with some titles on multiple fileparts
+        - Fix issue related to website exclusion inside title
+        
+        
         3.0.4 (2019-06-04)
         ------------------
         
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit.egg-info/SOURCES.txt 
new/guessit-3.1.0/guessit.egg-info/SOURCES.txt
--- old/guessit-3.0.4/guessit.egg-info/SOURCES.txt      2019-06-04 
23:30:58.000000000 +0200
+++ new/guessit-3.1.0/guessit.egg-info/SOURCES.txt      2019-09-02 
22:11:14.000000000 +0200
@@ -32,6 +32,7 @@
 guessit.egg-info/zip-safe
 guessit/config/options.json
 guessit/rules/__init__.py
+guessit/rules/match_processors.py
 guessit/rules/processors.py
 guessit/rules/common/__init__.py
 guessit/rules/common/comparators.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/guessit.egg-info/requires.txt 
new/guessit-3.1.0/guessit.egg-info/requires.txt
--- old/guessit-3.0.4/guessit.egg-info/requires.txt     2019-06-04 
23:30:58.000000000 +0200
+++ new/guessit-3.1.0/guessit.egg-info/requires.txt     2019-09-02 
22:11:14.000000000 +0200
@@ -1,6 +1,7 @@
-rebulk
+rebulk==2.*
 babelfish
 python-dateutil
+six
 
 [dev]
 zest.releaser[recommended]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/setup.py new/guessit-3.1.0/setup.py
--- old/guessit-3.0.4/setup.py  2019-06-04 23:30:57.000000000 +0200
+++ new/guessit-3.1.0/setup.py  2019-09-02 22:11:13.000000000 +0200
@@ -16,7 +16,7 @@
 with io.open(os.path.join(here, 'HISTORY.rst'), encoding='utf-8') as f:
     history = f.read()
 
-install_requires = ['rebulk', 'babelfish', 'python-dateutil']
+install_requires = ['rebulk==2.*', 'babelfish', 'python-dateutil', 'six']
 
 setup_requires = ['pytest-runner']
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/guessit-3.0.4/tox.ini new/guessit-3.1.0/tox.ini
--- old/guessit-3.0.4/tox.ini   2019-06-04 23:30:57.000000000 +0200
+++ new/guessit-3.1.0/tox.ini   2019-09-02 22:11:13.000000000 +0200
@@ -1,5 +1,10 @@
 [tox]
-envlist = py27,py34,py35,py36,py37,pypy2,pypy
+envlist = py27,py34,py35,py36,py37,py38,pypy2,pypy
+
+[testenv:py38]
+commands =
+    {envbindir}/pip install -e .[dev]
+    {envpython} setup.py test
 
 [testenv]
 commands =


Reply via email to