Repository: sympy/sympy

Issue:
Classes in diffgeom module are better be Atom, not only Basic
Let me exemplify with `Manifold`. `Manifold` has two arguments: its name in `Symbol`, and its dimension. Although these arguments do make `Manifold` object unique, they do not comprise mathematical tree structure. This means they should not be substitutable nor counted as free symbol.

Currently, `Manifold`, `Patch` and `CoordSystem` are `Basic` object, not `Atom`. This makes such strange result:

```python
>>> from sympy.diffgeom import Manifold
>>> from sympy import symbols
>>> m, n = symbols('m n')
>>> manifold = Manifold('m', 2)
>>> manifold.subs(m, n) # Its name is substituted
Manifold(n, 2)
```

When it comes to `BaseScalarField`, such behavior can be actually treacherous:
```python
>>> from sympy.diffgeom.rn import R2_r # This 2D Cartesian coordinate
>>> from sympy import Symbol
>>> o = Symbol('origin')
>>> expr = R2_r.x + o # This is a scalar field, with no dependency on Symbol('origin')
>>> expr.subs(o,3) == R2_r.x + 3
False
```

This will be avoidable by changing their superclass from `Basic` to `Atom`. If this sounds OK, I will make a PR myself.

Discussion:

Patch:
diff --git a/sympy/diffgeom/diffgeom.py b/sympy/diffgeom/diffgeom.py
index 90c7b1f749..a4d1ed30b8 100644
--- a/sympy/diffgeom/diffgeom.py
+++ b/sympy/diffgeom/diffgeom.py
@@ -3,9 +3,13 @@
 from itertools import permutations
 
 from sympy.combinatorics import Permutation
-from sympy.core import AtomicExpr, Basic, Expr, Dummy, Function, sympify, diff, Pow, Mul, Add, symbols, Tuple
+from sympy.core import (
+    Basic, Expr, Dummy, Function,  diff,
+    Pow, Mul, Add, Atom
+)
 from sympy.core.compatibility import reduce
 from sympy.core.numbers import Zero
+from sympy.core.sympify import _sympify
 from sympy.functions import factorial
 from sympy.matrices import Matrix
 from sympy.simplify import simplify
@@ -19,18 +23,27 @@
 from sympy.tensor.array import ImmutableDenseNDimArray
 
 
-class Manifold(Basic):
-    """Object representing a mathematical manifold.
+class Manifold(Atom):
+    """A mathematical manifold.
+
+    Explanation
+    ===========
 
     The only role that this object plays is to keep a list of all patches
     defined on the manifold. It does not provide any means to study the
     topological characteristics of the manifold that it represents.
 
+    Parameters
+    ==========
+    name : str
+        The name of the manifold.
+
+    dim : int
+        The dimension of the manifold.
     """
+
     def __new__(cls, name, dim):
-        name = sympify(name)
-        dim = sympify(dim)
-        obj = Basic.__new__(cls, name, dim)
+        obj = super().__new__(cls)
         obj.name = name
         obj.dim = dim
         obj.patches = []
@@ -38,12 +51,14 @@ def __new__(cls, name, dim):
         # other Patch instance on the same manifold.
         return obj
 
-    def _latex(self, printer, *args):
-        return r'\text{%s}' % self.name
+    def _hashable_content(self):
+        return self.name, self.dim
 
+class Patch(Atom):
+    """A patch on a manifold.
 
-class Patch(Basic):
-    """Object representing a patch on a manifold.
+    Explanation
+    ===========
 
     On a manifold one can have many patches that do not always include the
     whole manifold. On these patches coordinate charts can be defined that
@@ -53,6 +68,14 @@ class Patch(Basic):
     This object serves as a container/parent for all coordinate system charts
     that can be defined on the patch it represents.
 
+    Parameters
+    ==========
+    name : string
+        The name of the patch.
+
+    manifold : Manifold
+        The manifold on which the patch is defined.
+
     Examples
     ========
 
@@ -68,8 +91,7 @@ class Patch(Basic):
     # Contains a reference to the parent manifold in order to be able to access
     # other patches.
     def __new__(cls, name, manifold):
-        name = sympify(name)
-        obj = Basic.__new__(cls, name, manifold)
+        obj = super().__new__(cls)
         obj.name = name
         obj.manifold = manifold
         obj.manifold.patches.append(obj)
@@ -82,12 +104,28 @@ def __new__(cls, name, manifold):
     def dim(self):
         return self.manifold.dim
 
-    def _latex(self, printer, *args):
-        return r'\text{%s}_{%s}' % (self.name, self.manifold._latex(printer, *args))
+    def _hashable_content(self):
+        return self.name, self.manifold
+
+class CoordSystem(Atom):
+    """A coordinate system defined on the patch
+
+    Explanation
+    ===========
 
+    This class contains all coordinate transformation logic.
 
-class CoordSystem(Basic):
-    """Contains all coordinate transformation logic.
+    Parameters
+    ==========
+
+    name : string
+        The name of the coordinate system.
+
+    patch : Patch
+        The patch where the coordinate system is defined.
+
+    names : list of strings, optional
+        Determines how base scalar fields will be printed.
 
     Examples
     ========
@@ -179,18 +217,13 @@ class CoordSystem(Basic):
     #  Contains a reference to the parent patch in order to be able to access
     # other coordinate system charts.
     def __new__(cls, name, patch, names=None):
-        name = sympify(name)
         # names is not in args because it is related only to printing, not to
         # identifying the CoordSystem instance.
         if not names:
             names = ['%s_%d' % (name, i) for i in range(patch.dim)]
-        if isinstance(names, Tuple):
-            obj = Basic.__new__(cls, name, patch, names)
-        else:
-            names = Tuple(*symbols(names))
-            obj = Basic.__new__(cls, name, patch, names)
+        obj = super().__new__(cls)
         obj.name = name
-        obj._names = [str(i) for i in names.args]
+        obj._names = tuple(str(i) for i in names)
         obj.patch = patch
         obj.patch.coord_systems.append(obj)
         obj.transforms = {}
@@ -209,6 +242,9 @@ def __new__(cls, name, patch, names=None):
     def dim(self):
         return self.patch.dim
 
+    def _hashable_content(self):
+        return self.name, self.patch, self._names
+
     ##########################################################################
     # Coordinate transformations.
     ##########################################################################
@@ -340,13 +376,11 @@ def point_to_coords(self, point):
     # Printing.
     ##########################################################################
 
-    def _latex(self, printer, *args):
-        return r'\text{%s}^{\text{%s}}_{%s}' % (
-            self.name, self.patch.name, self.patch.manifold._latex(printer, *args))
-
-
 class Point(Basic):
-    """Point in a Manifold object.
+    """Point defined in a coordinate system.
+
+    Explanation
+    ===========
 
     To define a point you must supply coordinates and a coordinate system.
 
@@ -355,6 +389,14 @@ class Point(Basic):
     limitations in the simplification routines you can arrive at complicated
     expressions if you use inappropriate coordinate systems.
 
+    Parameters
+    ==========
+
+    coord_sys : CoordSystem
+
+    coords: list of sympy expressions
+        The coordinates of the point.
+
     Examples
     ========
 
@@ -383,11 +425,12 @@ class Point(Basic):
     [ sqrt(2)*r/2]])
 
     """
-    def __init__(self, coord_sys, coords):
-        super().__init__()
-        self._coord_sys = coord_sys
-        self._coords = Matrix(coords)
-        self._args = self._coord_sys, self._coords
+    def __new__(cls, coord_sys, coords):
+        coords = Matrix(coords)
+        obj = super().__new__(cls, coord_sys, coords)
+        obj._coord_sys = coord_sys
+        obj._coords = coords
+        return obj
 
     def coords(self, to_sys=None):
         """Coordinates of the point in a given coordinate system.
@@ -404,9 +447,12 @@ def free_symbols(self):
         return self._coords.free_symbols
 
 
-class BaseScalarField(AtomicExpr):
+class BaseScalarField(Expr):
     """Base Scalar Field over a Manifold for a given Coordinate System.
 
+    Explanation
+    ===========
+
     A scalar field takes a point as an argument and returns a scalar.
 
     A base scalar field of a coordinate system takes a point and returns one of
@@ -423,6 +469,13 @@ class BaseScalarField(AtomicExpr):
     You can build complicated scalar fields by just building up SymPy
     expressions containing ``BaseScalarField`` instances.
 
+    Parameters
+    ==========
+
+    coord_sys : CoordSystem
+
+    index : integer
+
     Examples
     ========
 
@@ -460,7 +513,8 @@ class BaseScalarField(AtomicExpr):
     is_commutative = True
 
     def __new__(cls, coord_sys, index):
-        obj = AtomicExpr.__new__(cls, coord_sys, sympify(index))
+        index = _sympify(index)
+        obj = super().__new__(cls, coord_sys, index)
         obj._coord_sys = coord_sys
         obj._index = index
         return obj
@@ -488,9 +542,12 @@ def doit(self):
         return self
 
 
-class BaseVectorField(AtomicExpr):
+class BaseVectorField(Expr):
     r"""Vector Field over a Manifold.
 
+    Explanation
+    ===========
+
     A vector field is an operator taking a scalar field and returning a
     directional derivative (which is also a scalar field).
 
@@ -505,6 +562,13 @@ class BaseVectorField(AtomicExpr):
     simplification routines you may arrive at more complicated expression if you
     use unappropriate coordinate systems.
 
+    Parameters
+    ==========
+
+    coord_sys : CoordSystem
+
+    index : integer
+
     Examples
     ========
 
@@ -551,8 +615,8 @@ class BaseVectorField(AtomicExpr):
     is_commutative = False
 
     def __new__(cls, coord_sys, index):
-        index = sympify(index)
-        obj = AtomicExpr.__new__(cls, coord_sys, index)
+        index = _sympify(index)
+        obj = super().__new__(cls, coord_sys, index)
         obj._coord_sys = coord_sys
         obj._index = index
         return obj
@@ -595,6 +659,13 @@ def __call__(self, scalar_field):
         result = result.subs(list(zip(coords, self._coord_sys.coord_functions())))
         return result.doit()
 
+def _find_coords(expr):
+    # Finds CoordinateSystems existing in expr
+    fields = expr.atoms(BaseScalarField, BaseVectorField)
+    result = set()
+    for f in fields:
+        result.add(f._coord_sys)
+    return result
 
 class Commutator(Expr):
     r"""Commutator of two vector fields.
@@ -627,7 +698,7 @@ class Commutator(Expr):
     Commutator(e_x, e_r)
 
     >>> simplify(c_xr(R2.y**2))
-    -2*y**2*cos(theta)/(x**2 + y**2)
+    -2*cos(theta)*y**2/(x**2 + y**2)
 
     """
     def __new__(cls, v1, v2):
@@ -637,7 +708,7 @@ def __new__(cls, v1, v2):
                 'Only commutators of vector fields are supported.')
         if v1 == v2:
             return Zero()
-        coord_sys = set().union(*[v.atoms(CoordSystem) for v in (v1, v2)])
+        coord_sys = set().union(*[_find_coords(v) for v in (v1, v2)])
         if len(coord_sys) == 1:
             # Only one coordinate systems is used, hence it is easy enough to
             # actually evaluate the commutator.
@@ -1082,9 +1153,6 @@ def __call__(self, field):
                     for v in vectors]
         return self._wrt.subs(list(zip(vectors, base_ops))).rcall(field)
 
-    def _latex(self, printer, *args):
-        return r'\mathbb{\nabla}_{%s}' % printer._print(self._wrt)
-
 
 ###############################################################################
 # Integral curves on vector fields
@@ -1295,7 +1363,7 @@ def dummyfy(args, exprs):
     # TODO Is this a good idea?
     d_args = Matrix([s.as_dummy() for s in args])
     reps = dict(zip(args, d_args))
-    d_exprs = Matrix([sympify(expr).subs(reps) for expr in exprs])
+    d_exprs = Matrix([_sympify(expr).subs(reps) for expr in exprs])
     return d_args, d_exprs
 
 
@@ -1407,7 +1475,7 @@ def vectors_in_basis(expr, to_sys):
     >>> from sympy.diffgeom import vectors_in_basis
     >>> from sympy.diffgeom.rn import R2_r, R2_p
     >>> vectors_in_basis(R2_r.e_x, R2_p)
-    x*e_r/sqrt(x**2 + y**2) - y*e_theta/(x**2 + y**2)
+    -y*e_theta/(x**2 + y**2) + x*e_r/sqrt(x**2 + y**2)
     >>> vectors_in_basis(R2_p.e_r, R2_r)
     sin(theta)*e_y + cos(theta)*e_x
     """
@@ -1453,7 +1521,7 @@ def twoform_to_matrix(expr):
     """
     if covariant_order(expr) != 2 or contravariant_order(expr):
         raise ValueError('The input expression is not a two-form.')
-    coord_sys = expr.atoms(CoordSystem)
+    coord_sys = _find_coords(expr)
     if len(coord_sys) != 1:
         raise ValueError('The input expression concerns more than one '
                          'coordinate systems, hence there is no unambiguous '
@@ -1488,7 +1556,7 @@ def metric_to_Christoffel_1st(expr):
     if not matrix.is_symmetric():
         raise ValueError(
             'The two-form representing the metric is not symmetric.')
-    coord_sys = expr.atoms(CoordSystem).pop()
+    coord_sys = _find_coords(expr).pop()
     deriv_matrices = [matrix.applyfunc(lambda a: d(a))
                       for d in coord_sys.base_vectors()]
     indices = list(range(coord_sys.dim))
@@ -1518,7 +1586,7 @@ def metric_to_Christoffel_2nd(expr):
 
     """
     ch_1st = metric_to_Christoffel_1st(expr)
-    coord_sys = expr.atoms(CoordSystem).pop()
+    coord_sys = _find_coords(expr).pop()
     indices = list(range(coord_sys.dim))
     # XXX workaround, inverting a matrix does not work if it contains non
     # symbols
@@ -1558,16 +1626,16 @@ def metric_to_Riemann_components(expr):
     >>> non_trivial_metric = exp(2*R2.r)*TP(R2.dr, R2.dr) + \
         R2.r**2*TP(R2.dtheta, R2.dtheta)
     >>> non_trivial_metric
-    r**2*TensorProduct(dtheta, dtheta) + exp(2*r)*TensorProduct(dr, dr)
+    exp(2*r)*TensorProduct(dr, dr) + r**2*TensorProduct(dtheta, dtheta)
     >>> riemann = metric_to_Riemann_components(non_trivial_metric)
     >>> riemann[0, :, :, :]
-    [[[0, 0], [0, 0]], [[0, r*exp(-2*r)], [-r*exp(-2*r), 0]]]
+    [[[0, 0], [0, 0]], [[0, exp(-2*r)*r], [-exp(-2*r)*r, 0]]]
     >>> riemann[1, :, :, :]
     [[[0, -1/r], [1/r, 0]], [[0, 0], [0, 0]]]
 
     """
     ch_2nd = metric_to_Christoffel_2nd(expr)
-    coord_sys = expr.atoms(CoordSystem).pop()
+    coord_sys = _find_coords(expr).pop()
     indices = list(range(coord_sys.dim))
     deriv_ch = [[[[d(ch_2nd[i, j, k])
                    for d in coord_sys.base_vectors()]
@@ -1612,13 +1680,13 @@ def metric_to_Ricci_components(expr):
     >>> non_trivial_metric = exp(2*R2.r)*TP(R2.dr, R2.dr) + \
                              R2.r**2*TP(R2.dtheta, R2.dtheta)
     >>> non_trivial_metric
-    r**2*TensorProduct(dtheta, dtheta) + exp(2*r)*TensorProduct(dr, dr)
+    exp(2*r)*TensorProduct(dr, dr) + r**2*TensorProduct(dtheta, dtheta)
     >>> metric_to_Ricci_components(non_trivial_metric)
-    [[1/r, 0], [0, r*exp(-2*r)]]
+    [[1/r, 0], [0, exp(-2*r)*r]]
 
     """
     riemann = metric_to_Riemann_components(expr)
-    coord_sys = expr.atoms(CoordSystem).pop()
+    coord_sys = _find_coords(expr).pop()
     indices = list(range(coord_sys.dim))
     ricci = [[Add(*[riemann[k, i, k, j] for k in indices])
               for j in indices]
diff --git a/sympy/printing/latex.py b/sympy/printing/latex.py
index 7bcbe50bbb..f0d4d492aa 100644
--- a/sympy/printing/latex.py
+++ b/sympy/printing/latex.py
@@ -2478,6 +2478,20 @@ def _print_MatrixHomomorphism(self, h):
         return r"{{{}}} : {{{}}} \to {{{}}}".format(self._print(h._sympy_matrix()),
             self._print(h.domain), self._print(h.codomain))
 
+    def _print_Manifold(self, manifold):
+        return r'\text{%s}' % manifold.name
+
+    def _print_Patch(self, patch):
+        return r'\text{%s}_{\text{%s}}' % (patch.name, patch.manifold.name)
+
+    def _print_CoordSystem(self, coords):
+        return r'\text{%s}^{\text{%s}}_{\text{%s}}' % (
+            coords.name, coords.patch.name, coords.patch.manifold.name
+        )
+
+    def _print_CovarDerivativeOp(self, cvd):
+        return r'\mathbb{\nabla}_{%s}' % self._print(cvd._wrt)
+
     def _print_BaseScalarField(self, field):
         string = field._coord_sys._names[field._index]
         return r'\mathbf{{{}}}'.format(self._print(Symbol(string)))
@@ -2544,7 +2558,6 @@ def _print_primeomega(self, expr, exp=None):
                 (self._print(expr.args[0]), self._print(exp))
         return r'\Omega\left(%s\right)' % self._print(expr.args[0])
 
-
 def translate(s):
     r'''
     Check for a modifier ending the string.  If present, convert the
diff --git a/sympy/printing/pretty/pretty.py b/sympy/printing/pretty/pretty.py
index 3fc7d40ceb..01802f0839 100644
--- a/sympy/printing/pretty/pretty.py
+++ b/sympy/printing/pretty/pretty.py
@@ -2574,6 +2574,15 @@ def _print_MatrixHomomorphism(self, h):
             ' %s> ' % hobj('-', 2), self._print(h.codomain)))
         return pform
 
+    def _print_Manifold(self, manifold):
+        return self._print(manifold.name)
+
+    def _print_Patch(self, patch):
+        return self._print(patch.name)
+
+    def _print_CoordSystem(self, coords):
+        return self._print(coords.name)
+
     def _print_BaseScalarField(self, field):
         string = field._coord_sys._names[field._index]
         return self._print(pretty_symbol(string))
@@ -2633,7 +2642,6 @@ def _print_AssignmentBase(self, e):
         pform = prettyForm(*stringPict.next(l, op, r))
         return pform
 
-
 def pretty(expr, **settings):
     """Returns a string containing the prettified form of expr.
 
diff --git a/sympy/printing/repr.py b/sympy/printing/repr.py
index 68a3dbab46..c936a692c0 100644
--- a/sympy/printing/repr.py
+++ b/sympy/printing/repr.py
@@ -315,6 +315,30 @@ def _print_ExtensionElement(self, f):
         ext = self._print(f.ext)
         return "ExtElem(%s, %s)" % (rep, ext)
 
+    def _print_Manifold(self, manifold):
+        class_name = manifold.func.__name__
+        name = self._print(manifold.name)
+        dim = self._print(manifold.dim)
+        return "%s(%s, %s)" % (class_name, name, dim)
+
+    def _print_Patch(self, patch):
+        class_name = patch.func.__name__
+        name = self._print(patch.name)
+        manifold = self._print(patch.manifold)
+        return "%s(%s, %s)" % (class_name, name, manifold)
+
+    def _print_CoordSystem(self, coords):
+        class_name = coords.func.__name__
+        name = self._print(coords.name)
+        patch = self._print(coords.patch)
+        names = self._print(coords._names)
+        return "%s(%s, %s, %s)" % (class_name, name, patch, names)
+
+    def _print_BaseScalarField(self, bsf):
+        class_name = bsf.func.__name__
+        coords = self._print(bsf._coord_sys)
+        idx = self._print(bsf._index)
+        return "%s(%s, %s)" % (class_name, coords, idx)
 
 def srepr(expr, **settings):
     """return expr in repr form"""
diff --git a/sympy/printing/str.py b/sympy/printing/str.py
index ea161d53e1..ba6203598f 100644
--- a/sympy/printing/str.py
+++ b/sympy/printing/str.py
@@ -863,6 +863,15 @@ def _print_NamedMorphism(self, morphism):
     def _print_Category(self, category):
         return 'Category("%s")' % category.name
 
+    def _print_Manifold(self, manifold):
+        return manifold.name
+
+    def _print_Patch(self, patch):
+        return patch.name
+
+    def _print_CoordSystem(self, coords):
+        return coords.name
+
     def _print_BaseScalarField(self, field):
         return field._coord_sys._names[field._index]
 
@@ -880,7 +889,6 @@ def _print_Tr(self, expr):
         #TODO : Handle indices
         return "%s(%s)" % ("Tr", self._print(expr.args[0]))
 
-
 def sstr(expr, **settings):
     """Returns the expression as a string.
 


Test patch:
diff --git a/sympy/diffgeom/tests/test_class_structure.py b/sympy/diffgeom/tests/test_class_structure.py
index a7e9834fd3..76d3f78efa 100644
--- a/sympy/diffgeom/tests/test_class_structure.py
+++ b/sympy/diffgeom/tests/test_class_structure.py
@@ -20,16 +20,17 @@ def test_point():
     #TODO assert point.free_symbols == set([x, y])
 
 
+def test_atomicclass_args():
+    assert m.args == ()
+    assert p.args == ()
+    assert cs.args == ()
+    assert cs_noname.args == ()
+
 def test_rebuild():
-    assert m == m.func(*m.args)
-    assert p == p.func(*p.args)
-    assert cs == cs.func(*cs.args)
-    assert cs_noname == cs_noname.func(*cs_noname.args)
     assert s1 == s1.func(*s1.args)
     assert v1 == v1.func(*v1.args)
     assert f1 == f1.func(*f1.args)
 
-
 def test_subs():
     assert s1.subs(s1, s2) == s2
     assert v1.subs(v1, v2) == v2
diff --git a/sympy/printing/pretty/tests/test_pretty.py b/sympy/printing/pretty/tests/test_pretty.py
index 69d6958683..09f751c759 100644
--- a/sympy/printing/pretty/tests/test_pretty.py
+++ b/sympy/printing/pretty/tests/test_pretty.py
@@ -7003,3 +7003,14 @@ def test_issue_18272():
     '⎪                 ⎜⎜⎪   x              ⎟          ⎟⎪\n'\
     '⎪                 ⎜⎜⎪   ─     otherwise⎟          ⎟⎪\n'\
     '⎩                 ⎝⎝⎩   2              ⎠          ⎠⎭'
+
+def test_diffgeom():
+    from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField
+    m = Manifold('M', 2)
+    assert pretty(m) == 'M'
+    p = Patch('P', m)
+    assert pretty(p) == "P"
+    rect = CoordSystem('rect', p)
+    assert pretty(rect) == "rect"
+    b = BaseScalarField(rect, 0)
+    assert pretty(b) == "rect_0"
diff --git a/sympy/printing/tests/test_latex.py b/sympy/printing/tests/test_latex.py
index 157bcb1241..a418e4d0c9 100644
--- a/sympy/printing/tests/test_latex.py
+++ b/sympy/printing/tests/test_latex.py
@@ -2528,7 +2528,7 @@ def test_text_re_im():
     assert latex(re(x), gothic_re_im=False) ==  r'\operatorname{re}{\left(x\right)}'
 
 
-def test_DiffGeomMethods():
+def test_latex_diffgeom():
     from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField, Differential
     from sympy.diffgeom.rn import R2
     m = Manifold('M', 2)
diff --git a/sympy/printing/tests/test_repr.py b/sympy/printing/tests/test_repr.py
index 490042087f..f749683ff7 100644
--- a/sympy/printing/tests/test_repr.py
+++ b/sympy/printing/tests/test_repr.py
@@ -307,3 +307,14 @@ def test_Cycle():
 def test_Permutation():
     import_stmt = "from sympy.combinatorics import Permutation"
     sT(Permutation(1, 2), "Permutation(1, 2)", import_stmt)
+
+def test_diffgeom():
+    from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField
+    m = Manifold('M', 2)
+    assert srepr(m) == "Manifold('M', 2)"
+    p = Patch('P', m)
+    assert srepr(p) == "Patch('P', Manifold('M', 2))"
+    rect = CoordSystem('rect', p)
+    assert srepr(rect) == "CoordSystem('rect', Patch('P', Manifold('M', 2)), ('rect_0', 'rect_1'))"
+    b = BaseScalarField(rect, 0)
+    assert srepr(b) == "BaseScalarField(CoordSystem('rect', Patch('P', Manifold('M', 2)), ('rect_0', 'rect_1')), Integer(0))"
diff --git a/sympy/printing/tests/test_str.py b/sympy/printing/tests/test_str.py
index c1247eabda..082b7844d4 100644
--- a/sympy/printing/tests/test_str.py
+++ b/sympy/printing/tests/test_str.py
@@ -889,3 +889,14 @@ def test_str_special_matrices():
 
 def test_issue_14567():
     assert factorial(Sum(-1, (x, 0, 0))) + y  # doesn't raise an error
+
+def test_diffgeom():
+    from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField
+    m = Manifold('M', 2)
+    assert str(m) == "M"
+    p = Patch('P', m)
+    assert str(p) == "P"
+    rect = CoordSystem('rect', p)
+    assert str(rect) == "rect"
+    b = BaseScalarField(rect, 0)
+    assert str(b) == "rect_0"