diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 918d5a490920a60f9f3b5637381b1ff842aebecd..b4cd23f7aaf0ca551ccca40a62655464e6c86335 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,7 @@ jobs:
         -   name: "Main Script"
             run: |
                 curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-flake8.sh
-                . ./prepare-and-run-flake8.sh ./grudge ./examples ./test
+                . ./prepare-and-run-flake8.sh "$(basename $GITHUB_REPOSITORY)" examples test
 
     pytest2:
         name: Pytest on Py2
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1d877ddbb97c6feec650d7b6648474017eb2e837..c059c7a92c38135ef24ca83ac4a5cb48dfebea82 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -157,7 +157,7 @@ Documentation:
 Flake8:
   script:
   - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-flake8.sh
-  - ". ./prepare-and-run-flake8.sh grudge examples test"
+  - . ./prepare-and-run-flake8.sh "$CI_PROJECT_NAME" examples test
   tags:
   - python3
   except:
diff --git a/grudge/discretization.py b/grudge/discretization.py
index 20fc05053dd4cca70ee29b4909b045c52abc04e7..d24fb2c082ddf3585d7de13f58a7a0b371cc4ed0 100644
--- a/grudge/discretization.py
+++ b/grudge/discretization.py
@@ -131,9 +131,10 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
             # FIXME
             raise NotImplementedError("Distributed communication with quadrature")
 
-        assert isinstance(dd.domain_tag, sym.BTAG_PARTITION)
+        assert isinstance(dd.domain_tag, sym.DTAG_BOUNDARY)
+        assert isinstance(dd.domain_tag.tag, sym.BTAG_PARTITION)
 
-        return self._dist_boundary_connections[dd.domain_tag.part_nr]
+        return self._dist_boundary_connections[dd.domain_tag.tag.part_nr]
 
     @memoize_method
     def discr_from_dd(self, dd):
@@ -161,8 +162,8 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
             return self._all_faces_volume_connection().to_discr
         elif dd.domain_tag is sym.FACE_RESTR_INTERIOR:
             return self._interior_faces_connection().to_discr
-        elif dd.is_boundary():
-            return self._boundary_connection(dd.domain_tag).to_discr
+        elif dd.is_boundary_or_partition_interface():
+            return self._boundary_connection(dd.domain_tag.tag).to_discr
         else:
             raise ValueError("DOF desc tag not understood: " + str(dd))
 
@@ -239,9 +240,9 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
                 return self._all_faces_volume_connection()
             if to_dd.domain_tag is sym.FACE_RESTR_INTERIOR:
                 return self._interior_faces_connection()
-            elif to_dd.is_boundary():
+            elif to_dd.is_boundary_or_partition_interface():
                 assert from_dd.quadrature_tag is sym.QTAG_NONE
-                return self._boundary_connection(to_dd.domain_tag)
+                return self._boundary_connection(to_dd.domain_tag.tag)
             elif to_dd.is_volume():
                 from meshmode.discretization.connection import \
                         make_same_mesh_connection
@@ -385,6 +386,8 @@ class PointsDiscretization(DiscretizationBase):
                 np.float64: np.complex128
         }[self.real_dtype.type])
 
+        self.mpi_communicator = None
+
     def ambient_dim(self):
         return self._nodes.shape[0]
 
@@ -399,6 +402,10 @@ class PointsDiscretization(DiscretizationBase):
     def nodes(self):
         return self._nodes
 
+    @property
+    def facial_adjacency_groups(self):
+        return []
+
     def discr_from_dd(self, dd):
         dd = sym.as_dofdesc(dd)
 
diff --git a/grudge/models/wave.py b/grudge/models/wave.py
index 0ac60a336a4f72f12ba4f3eeb0bc2d79c9f07509..6defafc878f856fe83449b12dc055cfdf0b1f432 100644
--- a/grudge/models/wave.py
+++ b/grudge/models/wave.py
@@ -163,7 +163,9 @@ class StrongWaveOperator(HyperbolicOperator):
                         + flux(sym.bv_tpair(self.dirichlet_tag, w, dir_bc))
                         + flux(sym.bv_tpair(self.neumann_tag, w, neu_bc))
                         + flux(sym.bv_tpair(self.radiation_tag, w, rad_bc))
-                        )))
+                        )
+                    )
+                )
 
         result[0] += self.source_f
 
diff --git a/grudge/symbolic/mappers/__init__.py b/grudge/symbolic/mappers/__init__.py
index b277c4c99e97280e4f82be63cac8bbcbb00bee8e..8652e392f917e8beecae30d01f84fd52e9f7e2aa 100644
--- a/grudge/symbolic/mappers/__init__.py
+++ b/grudge/symbolic/mappers/__init__.py
@@ -566,51 +566,6 @@ class OperatorSpecializer(CSECachingMapperMixin, IdentityMapper):
             raise TypeError("RestrictToBoundary cannot be applied to "
                     "quadrature-based operands--use QuadUpsample(Boundarize(...))")
 
-        # {{{ flux operator specialization
-        elif isinstance(expr.op, op.FluxOperatorBase):
-            from pytools.obj_array import with_object_array_or_scalar
-
-            repr_tag_cell = [None]
-
-            def process_flux_arg(flux_arg):
-                arg_repr_tag = self.typedict[flux_arg].repr_tag
-                if repr_tag_cell[0] is None:
-                    repr_tag_cell[0] = arg_repr_tag
-                else:
-                    # An error for this condition is generated by
-                    # the type inference pass.
-
-                    assert arg_repr_tag == repr_tag_cell[0]
-
-            is_boundary = isinstance(expr.field, BoundaryPair)
-            if is_boundary:
-                bpair = expr.field
-                with_object_array_or_scalar(process_flux_arg, bpair.field)
-                with_object_array_or_scalar(process_flux_arg, bpair.bfield)
-            else:
-                with_object_array_or_scalar(process_flux_arg, expr.field)
-
-            is_quad = isinstance(repr_tag_cell[0], QuadratureRepresentation)
-            if is_quad:
-                assert not expr.op.is_lift
-                quad_tag = repr_tag_cell[0].quadrature_tag
-
-            new_fld = self.rec(expr.field)
-            flux = expr.op.flux
-
-            if is_boundary:
-                if is_quad:
-                    return op.QuadratureBoundaryFluxOperator(
-                            flux, quad_tag, bpair.tag)(new_fld)
-                else:
-                    return op.BoundaryFluxOperator(flux, bpair.tag)(new_fld)
-            else:
-                if is_quad:
-                    return op.QuadratureFluxOperator(flux, quad_tag)(new_fld)
-                else:
-                    return op.FluxOperator(flux, expr.op.is_lift)(new_fld)
-        # }}}
-
         else:
             return IdentityMapper.map_operator_binding(self, expr)
 
@@ -977,14 +932,13 @@ class EmptyFluxKiller(CSECachingMapperMixin, IdentityMapper):
     def map_operator_binding(self, expr):
         from meshmode.mesh import is_boundary_tag_empty
         if (isinstance(expr.op, sym.InterpolationOperator)
-                and expr.op.dd_out.is_boundary()
-                and expr.op.dd_out.domain_tag not in [
-                    sym.FACE_RESTR_ALL, sym.FACE_RESTR_INTERIOR]
-                and is_boundary_tag_empty(self.mesh,
-                    expr.op.dd_out.domain_tag)):
-            return 0
-        else:
-            return IdentityMapper.map_operator_binding(self, expr)
+                and expr.op.dd_out.is_boundary_or_partition_interface()):
+            domain_tag = expr.op.dd_out.domain_tag
+            assert isinstance(domain_tag, sym.DTAG_BOUNDARY)
+            if is_boundary_tag_empty(self.mesh, domain_tag.tag):
+                return 0
+
+        return IdentityMapper.map_operator_binding(self, expr)
 
 
 class _InnerDerivativeJoiner(pymbolic.mapper.RecursiveMapper):
diff --git a/grudge/symbolic/operators.py b/grudge/symbolic/operators.py
index 56a26feef896bc9599f8358a8a5dc0d792456825..b5d8407349a33b47ee115e918bef110f1932e73c 100644
--- a/grudge/symbolic/operators.py
+++ b/grudge/symbolic/operators.py
@@ -536,12 +536,15 @@ class OppositePartitionFaceSwap(Operator):
             dd_out = dd_in
 
         super(OppositePartitionFaceSwap, self).__init__(dd_in, dd_out)
-        if not isinstance(self.dd_in.domain_tag, prim.BTAG_PARTITION):
-            raise ValueError("dd_in must be a partition boundary faces domain")
+        if not (isinstance(self.dd_in.domain_tag, prim.DTAG_BOUNDARY)
+                and isinstance(self.dd_in.domain_tag.tag, prim.BTAG_PARTITION)):
+            raise ValueError(
+                    "dd_in must be a partition boundary faces domain, not '%s'"
+                    % self.dd_in.domain_tag)
         if self.dd_out != self.dd_in:
             raise ValueError("dd_out and dd_in must be identical")
 
-        self.i_remote_part = self.dd_in.domain_tag.part_nr
+        self.i_remote_part = self.dd_in.domain_tag.tag.part_nr
 
         assert unique_id is None or isinstance(unique_id, int)
         self.unique_id = unique_id
diff --git a/grudge/symbolic/primitives.py b/grudge/symbolic/primitives.py
index aa00e8e678be045fa5ae3938a295af5da3f9ff41..2318b70fbca1e4d4d0fa3428d491d53e10baae7e 100644
--- a/grudge/symbolic/primitives.py
+++ b/grudge/symbolic/primitives.py
@@ -125,6 +125,18 @@ class DTAG_BOUNDARY:        # noqa: N801
     def __init__(self, tag):
         self.tag = tag
 
+    def __eq__(self, other):
+        return isinstance(other, DTAG_BOUNDARY) and self.tag == other.tag
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __hash__(self):
+        return hash(type(self)) ^ hash(self.tag)
+
+    def __repr__(self):
+        return "<%s(%s)>" % (type(self).__name__, repr(self.tag))
+
 
 class QTAG_NONE:            # noqa: N801
     pass
@@ -139,7 +151,7 @@ class DOFDesc(object):
     .. automethod:: is_scalar
     .. automethod:: is_discretized
     .. automethod:: is_volume
-    .. automethod:: is_boundary
+    .. automethod:: is_boundary_or_partition_interface
     .. automethod:: is_trace
 
     .. automethod:: uses_quadrature
@@ -186,10 +198,9 @@ class DOFDesc(object):
         elif domain_tag in [FACE_RESTR_INTERIOR, "int_faces"]:
             domain_tag = FACE_RESTR_INTERIOR
         elif isinstance(domain_tag, BTAG_PARTITION):
-            pass
+            domain_tag = DTAG_BOUNDARY(domain_tag)
         elif domain_tag in [BTAG_ALL, BTAG_REALLY_ALL, BTAG_NONE]:
-            # FIXME: Should wrap these in DTAG_BOUNDARY
-            pass
+            domain_tag = DTAG_BOUNDARY(domain_tag)
         elif isinstance(domain_tag, DTAG_BOUNDARY):
             pass
         else:
@@ -213,15 +224,11 @@ class DOFDesc(object):
     def is_volume(self):
         return self.domain_tag is DTAG_VOLUME_ALL
 
-    def is_boundary(self):
-        return (
-                self.domain_tag in [
-                    BTAG_ALL, BTAG_NONE, BTAG_REALLY_ALL]
-                or isinstance(self.domain_tag, BTAG_PARTITION)
-                or isinstance(self.domain_tag, DTAG_BOUNDARY))
+    def is_boundary_or_partition_interface(self):
+        return isinstance(self.domain_tag, DTAG_BOUNDARY)
 
     def is_trace(self):
-        return (self.is_boundary()
+        return (self.is_boundary_or_partition_interface()
                 or self.domain_tag in [
                     FACE_RESTR_ALL,
                     FACE_RESTR_INTERIOR])
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..01a22d375f3828a580f3c2cdb5e5fde68ead896f
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+markers =
+    mpi: marks tests as using MPI
diff --git a/test/test_mpi_communication.py b/test/test_mpi_communication.py
index 7ed5233d2ab437eefcb5a87aa69f4d3a18da951a..9327bd514eeb7cbf9f2d9b4c725b33f80bb70307 100644
--- a/test/test_mpi_communication.py
+++ b/test/test_mpi_communication.py
@@ -293,7 +293,6 @@ if __name__ == "__main__":
         if len(sys.argv) > 1:
             exec(sys.argv[1])
         else:
-            from py.test.cmdline import main
+            from pytest import main
             main([__file__])
-
 # vim: fdm=marker