Repository: PyPSA/PyPSA

Issue:
Add function for auto-calculating line lengths from bus coords

method for updating component attributes
it could be useful to be able to change component attribute values after creating the network. Perhaps also for time-varying attributes. For me this would be useful when merging two networks with duplicate component indices, but I could imagine other use cases.
Components Class
Components are not pure dataframes but python classes which represent containers. For example, n.generators is a GeneratorContainer class or Generators class (an subclass of the ComponentContainer or Components) with various attributes like static for the static dataframe, dynamic for the timedependant dataframes, or even shortcuts, like n.generators.p which would directly return the time-dependent dispatch. These containers could have component specific front-end functions like add_ocgt which has all the technological data for OCGT power plants.

See discussion in #896 


Discussion:


Hi jessLryan, 

I don't understand the issue Isn't this possible, e.g. 
changing the maximum power (P_max) of a generator
`n.generators.loc['GeneratorID', 'p_nom_max'] = new_value`
or for time-varying attributes
`n.generators_t.p_max_pu.loc[:, 'GeneratorID'] = new_time_series`


@alexfias thanks for the comment. I don't think it will be complex, I'm just suggesting (and working on) a method that hopefully makes it easier. Any method that we write is also possible by writing out the code directly, otherwise it wouldn't be possible to write the method. Also if, for instance, a bus name is changed then this new name should be propagated elsewhere in the dataframes (that was my original incentive for writing this method).
@jessLryan What is the status here?
Could be interesting for #936 
@lkstrp sorry for the delay - no update on this, I've a little time to work on it, also happy for you to do so :)

Coming back to this point raised by @coroa 

> 1 Should Generators become the new full data container for what we have now at net.generators and net.generators_t? (I don't think it can, because it at least currently would have to know about net.snapshots)

I would say yes, that should be the goal. I think snapshots are a not a required information for intanitiating a `Generators` class. At first statge they are detached from any network. Snapshot compatibility would only be checked when adding them to the network. 


```python
import pypsa

snapshots = pd.date_range(...)
n = pypsa.Network(snapshots=snapshots)

p_max_pu = pd.DataFrame(..., columns=[1,2,3], index=snapshots) 
generators = pypsa.Generators(names=[1,2,3], p_nom=5, p_max_pu=p_max_pu)

n.add(generators) # here compatibility is checked
```





> This ambiguity you have right now as well. We just change the mapping.
`n.generators.p_max_pu` -> `n.generators.static.p_max_pu`
`n.generators_t.p_max_pu` -> `n.generators.varying.p_max_pu`
Right now you could have in the static one just 1s for each component and in the varying one a series at the same time. If this is the right approach is another discussion.

Yes that ambiguity exists currently, but i meant as a user of the interface. But you know that `n.generators.static.p_max_pu` returns a series, while `n.generators.varying.p_max_pu` returns a dataframe.

For `n.generators.p_max_pu`, I might receive either?
> Coming back to this point raised by @coroa
> 
> > 1 Should Generators become the new full data container for what we have now at net.generators and net.generators_t? (I don't think it can, because it at least currently would have to know about net.snapshots)
> 
> I would say yes, that should be the goal. I think snapshots are a not a required information for intanitiating a `Generators` class. At first statge they are detached from any network. Snapshot compatibility would only be checked when adding them to the network.
> 
> ```python
> import pypsa
> 
> snapshots = pd.date_range(...)
> n = pypsa.Network(snapshots=snapshots)
> 
> p_max_pu = pd.DataFrame(..., columns=[1,2,3], index=snapshots) 
> generators = pypsa.Generators(names=[1,2,3], p_nom=5, p_max_pu=p_max_pu)
> 
> n.add(generators) # here compatibility is checked
> ```

So underlying storage is a dataframe for static attributes and a dictionary of dataframes with varying attributes (with several supersets of snapshots), which are reindexed (or `pix.semijoin`ed ([doc string](https://pandas-indexing.readthedocs.io/en/latest/api.html#pandas_indexing.core.semijoin), and [user guide](https://pandas-indexing.readthedocs.io/en/latest/notebooks/introduction.html#selecting-based-on-a-multi-index))) on `net.add`.

Ok, yes, that might work.



Patch:
diff --git a/doc/examples-index/other.rst b/doc/examples-index/other.rst
index 7efdaa8ff..4ba9df191 100644
--- a/doc/examples-index/other.rst
+++ b/doc/examples-index/other.rst
@@ -15,3 +15,4 @@ This section provides examples related to different topics:
     ../examples/replace-generator-storage-units-with-store.ipynb
     ../examples/flow-plot.ipynb
     ../examples/stochastic-problem.ipynb
+    ../examples/experimental-components-class.ipynb
diff --git a/doc/examples/experimental-components-class.nblink b/doc/examples/experimental-components-class.nblink
new file mode 100644
index 000000000..813e05ccc
--- /dev/null
+++ b/doc/examples/experimental-components-class.nblink
@@ -0,0 +1,1 @@
+{"path": "../../examples/notebooks/experimental-components-class.ipynb"}
diff --git a/examples/notebooks/experimental-components-class.ipynb b/examples/notebooks/experimental-components-class.ipynb
new file mode 100644
index 000000000..b493416e9
--- /dev/null
+++ b/examples/notebooks/experimental-components-class.ipynb
@@ -0,0 +1,253 @@
+{
+ "cells": [
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Components class\n",
+    "\n",
+    "Version `0.33` of PyPSA introduces a structural refactoring of how component data is stored and accessed. The new structure adds an extra layer to move all component-specific data from the networks class to a new component class. With version `0.33`, most of these changes will be unnoticeable to the user. \n",
+    "\n",
+    "But this makes it easy to add new features. Below are some simple examples to show which other features could be added in the future. If you have any ideas, wishes, feedback or suggestions, please let us know via https://github.com/PyPSA/PyPSA/issues.\n",
+    "\n",
+    "Note that this is experimental, features will be added and the newly introduced API may change. You can still use PyPSA as usual. Major API changes will not be introduced before version `1.0`.\n",
+    "Also, while all classes and methods have docstrings, there is no dedicated documentation yet."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## General"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pypsa\n",
+    "\n",
+    "n = pypsa.examples.scigrid_de()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "So far, components data was directly attached to the network object (e.g. `n.generators`, `n.generators_t` etc.). While you still can access the data there, both actually sit now in a new `Components` class:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c = n.components.generators  # also via alias n.c.generators\n",
+    "c"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The datasets for static and dynamic data can be accessed via the class now, but also still via the old network properties:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c.static.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c.dynamic.keys()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Both ways refer to the same DataFrame/ Dict Container of\n",
+    "print(c.static is n.generators)\n",
+    "print(c.dynamic is n.generators_t)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Examples"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Simple alias properties"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Basic component information\n",
+    "print(f\"Component name: '{c.name}'\")\n",
+    "print(f\"Component list name: '{c.list_name}'\")\n",
+    "print(f\"Component type: '{c.type}'\")\n",
+    "\n",
+    "# Other maybe useful properties\n",
+    "print(f\"Nominal attribute: '{c.nominal_attr}'\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Quick access to attribute units\n",
+    "c.units.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Get ports of component (e.g. for multiport components)\n",
+    "n.c.links.ports"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Check if component is attached to network\n",
+    "if c.attached:\n",
+    "    print(f\"{c} is attached to {c.n}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Rename components and propagate new names through network"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Old names\n",
+    "print(f\"Old bus names: {', '.join(c.static.head(2).index)}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Rename \"1 Gas\" component\n",
+    "c = n.components.buses\n",
+    "rename_map = {\"1\": \"Super Bus\"}\n",
+    "c.rename_component_names(**rename_map)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# New names\n",
+    "print(f\"New bus names: {', '.join(c.static.head(2).index)}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Changes in other components of network\n",
+    "n.c.generators.static.head(2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Calculate line length from attached buses"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c = n.c.lines\n",
+    "c.calculate_line_length()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Those are just a couple of simple examples. Many other features could be added. If you have any ideas, wishes, feedback or suggestions, please let us know via the https://github.com/PyPSA/PyPSA/issues."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": []
+  }
+ ],
+ "metadata": {
+  "@webio": {
+   "lastCommId": null,
+   "lastKernelId": null
+  },
+  "kernelspec": {
+   "display_name": "",
+   "language": "python",
+   "name": ""
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.1"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/pypsa/components/abstract.py b/pypsa/components/abstract.py
index 36a3f57ac..9c8004e69 100644
--- a/pypsa/components/abstract.py
+++ b/pypsa/components/abstract.py
@@ -118,7 +118,7 @@ def __init__(
         static, dynamic = self._get_data_containers(ctype)
         super().__init__(ctype, n=None, static=static, dynamic=dynamic)
 
-    def __repr__(self) -> str:
+    def __str__(self) -> str:
         """
         Get string representation of component.
 
@@ -130,43 +130,27 @@ def __repr__(self) -> str:
         Examples
         --------
         >>> import pypsa
-        >>> c = pypsa.examples.ac_dc_meshed().components.generators
-        >>> c
-        PyPSA 'Generator' Components
-        ----------------------------
-        Attached to PyPSA Network 'AC-DC'
-        Components: 6
+        >>> n = pypsa.examples.ac_dc_meshed()
+        >>> str(n.components.generators)
+        "PyPSA 'Generator' Components"
 
         """
-        num_components = len(self.static)
-        if not num_components:
-            return f"Empty PyPSA {self.ctype.name} Components\n"
-        text = f"PyPSA '{self.ctype.name}' Components"
-        text += "\n" + "-" * len(text) + "\n"
+        return f"PyPSA '{self.ctype.name}' Components"
 
-        # Add attachment status
-        if self.attached:
-            network_name = f"'{self.n_save.name}'" if self.n_save.name else ""
-            text += f"Attached to PyPSA Network {network_name}\n"
-
-        text += f"Components: {len(self.static)}"
-
-        return text
-
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """
-        Get string representation of component.
+        Get representation of component.
 
         Returns
         -------
         str
-            String representation of component.
+            Representation of component.
 
         Examples
         --------
         >>> import pypsa
-        >>> n = pypsa.examples.ac_dc_meshed()
-        >>> n.components.generators
+        >>> c = pypsa.examples.ac_dc_meshed().components.generators
+        >>> c
         PyPSA 'Generator' Components
         ----------------------------
         Attached to PyPSA Network 'AC-DC'
@@ -174,7 +158,16 @@ def __str__(self) -> str:
 
         """
         num_components = len(self.static)
-        text = f"{num_components} '{self.ctype.name}' Components"
+        if not num_components:
+            return f"Empty {self}"
+        text = f"{self}\n" + "-" * len(str(self)) + "\n"
+
+        # Add attachment status
+        if self.attached:
+            text += f"Attached to {str(self.n)}\n"
+
+        text += f"Components: {len(self.static)}"
+
         return text
 
     def __getitem__(self, key: str) -> Any:
@@ -566,6 +559,147 @@ def pnl(self) -> dict:
         """
         return self.dynamic
 
+    @property
+    def units(self) -> pd.Series:
+        """
+        Get units of all attributes of components.
+
+        Returns
+        -------
+        pd.Series
+            Series with attribute names as index and units as values.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.ac_dc_meshed().components.generators
+        >>> c.units.head() # doctest: +SKIP
+                       unit
+        attribute
+        p_nom            MW
+        p_nom_mod        MW
+        p_nom_min        MW
+        p_nom_max        MW
+        p_min_pu   per unit
+
+        """
+        return self.defaults.unit[self.defaults.unit.notnull()].to_frame()
+
+    @property
+    def ports(self) -> list:
+        """
+        Get ports of all components.
+
+        Returns
+        -------
+        pd.Series
+            Series with attribute names as index and port names as values.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.ac_dc_meshed().components.lines
+        >>> c.ports
+        ['0', '1']
+
+        """
+        return [str(col)[3:] for col in self.static if str(col).startswith("bus")]
+
+    @property
+    def nominal_attr(self) -> str:
+        """
+        Get nominal attribute of component.
+
+        Returns
+        -------
+        str
+            Name of the nominal attribute of the component.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.ac_dc_meshed().components.generators
+        >>> c.nominal_attr
+        'p_nom'
+
+        """
+        # TODO: move to Component Specific class
+        nominal_attr = {
+            "Generator": "p_nom",
+            "Line": "s_nom",
+            "Transformer": "s_nom",
+            "Link": "p_nom",
+            "Store": "e_nom",
+            "StorageUnit": "p_nom",
+        }
+        try:
+            return nominal_attr[self.ctype.name]
+        except KeyError:
+            msg = f"Component type '{self.ctype.name}' has no nominal attribute."
+            raise AttributeError(msg)
+
+    # TODO move
+    def rename_component_names(self, **kwargs: str) -> None:
+        """
+        Rename component names.
+
+        Rename components and also update all cross-references of the component in
+        the network.
+
+        Parameters
+        ----------
+        **kwargs
+            Mapping of old names to new names.
+
+        Returns
+        -------
+        None
+
+        Examples
+        --------
+        Define some network
+        >>> import pypsa
+        >>> n = pypsa.Network()
+        >>> n.add("Bus", ["bus1"])
+        Index(['bus1'], dtype='object')
+        >>> n.add("Generator", ["gen1"], bus="bus1")
+        Index(['gen1'], dtype='object')
+        >>> c = n.c.buses
+
+        Now rename the bus
+
+        >>> c.rename_component_names(bus1="bus2")
+
+        Which updates the bus components
+
+        >>> c.static.index
+        Index(['bus2'], dtype='object', name='Bus')
+
+        and all references in the network
+
+        >>> n.generators.bus
+        Generator
+        gen1    bus2
+        Name: bus, dtype: object
+
+        """
+        if not all(isinstance(v, str) for v in kwargs.values()):
+            msg = "New names must be strings."
+            raise ValueError(msg)
+
+        # Rename component name definitions
+        self.static = self.static.rename(index=kwargs)
+        for k, v in self.dynamic.items():  # Modify in place
+            self.dynamic[k] = v.rename(columns=kwargs)
+
+        # Rename cross references in network (if attached to one)
+        if self.attached:
+            for c in self.n_save.components.values():
+                col_name = self.name.lower()  # TODO: Generalize
+                cols = [f"{col_name}{port}" for port in c.ports]
+                if cols and not c.static.empty:
+                    c.static[cols] = c.static[cols].replace(kwargs)
+
 
 class SubNetworkComponents:
     """
diff --git a/pypsa/components/components.py b/pypsa/components/components.py
index b7aa09dcf..2c4f1e198 100644
--- a/pypsa/components/components.py
+++ b/pypsa/components/components.py
@@ -11,12 +11,14 @@
 import warnings
 from typing import Any
 
+import numpy as np
 import pandas as pd
 
 from pypsa.components.abstract import Components
 from pypsa.components.types import ComponentType
 from pypsa.components.types import get as get_component_type
 from pypsa.definitions.structures import Dict
+from pypsa.geo import haversine_pts
 
 logger = logging.getLogger(__name__)
 
@@ -101,8 +103,100 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
         super().__init__(*args, **kwargs)
 
 
+class Lines(Components):
+    """
+    Lines components class.
+
+    This class is used for line components. All functionality specific to
+    lines is implemented here. Functionality for all components is implemented in
+    the abstract base class.
+
+    .. warning::
+        This class is under ongoing development and will be subject to changes.
+        It is not recommended to use this class outside of PyPSA.
+
+    See Also
+    --------
+    pypsa.components.abstract.Components : Base class for all components.
+    pypsa.components.components.GenericComponents : Generic components class.
+
+    """
+
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        """
+        Initialize Generators class.
+
+        See :class:`pypsa.components.abstract.Components` for more information.
+
+        Parameters
+        ----------
+        args : Any
+            Arguments of base class.
+        kwargs : Any
+            Keyword arguments of base class.
+
+        Returns
+        -------
+        None
+
+        """
+        super().__init__(*args, **kwargs)
+
+    def calculate_line_length(self) -> pd.Series:
+        """
+        Get length of the lines in meters.
+
+        Based on coordinates of attached buses. Buses must have 'x' and 'y' attributes,
+        otherwise no line length can be calculated. By default the haversine formula is
+        used to calculate the distance between two points.
+
+        Returns
+        -------
+        pd.Series
+            Length of the lines.
+
+        See Also
+        --------
+        pypsa.geo.haversine : Function to calculate distance between two points.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.scigrid_de().c.lines
+        >>> ds = c.calculate_line_length()
+        >>> ds.head()
+        0    34432.796096
+        1    59701.666027
+        2    32242.741010
+        3    30559.154647
+        4    21574.543367
+        dtype: float64
+
+        """
+        return (
+            pd.Series(
+                haversine_pts(
+                    a=np.array(
+                        [
+                            self.static.bus0.map(self.n_save.buses.x),
+                            self.static.bus0.map(self.n_save.buses.y),
+                        ]
+                    ).T,
+                    b=np.array(
+                        [
+                            self.static.bus1.map(self.n_save.buses.x),
+                            self.static.bus1.map(self.n_save.buses.y),
+                        ]
+                    ).T,
+                )
+            )
+            * 1_000
+        )
+
+
 CLASS_MAPPING = {
     "Generator": Generators,
+    "Line": Lines,
 }
 
 
diff --git a/pypsa/networks.py b/pypsa/networks.py
index 3740fa960..1fe95a88b 100644
--- a/pypsa/networks.py
+++ b/pypsa/networks.py
@@ -329,9 +329,11 @@ def __init__(
         for key, value in kwargs.items():
             setattr(self, key, value)
 
+    def __str__(self) -> str:
+        return f"PyPSA Network '{self.name}'" if self.name else "Unnamed PyPSA Network"
+
     def __repr__(self) -> str:
-        header = "PyPSA Network" + (f" '{self.name}'" if self.name else "")
-        header += "\n" + "-" * len(header)
+        header = f"{self}\n" + "-" * len(str(self))  # + "\n"
         comps = {
             c.name: f" - {c.name}: {len(c.static)}"
             for c in self.iterate_components()
@@ -1721,6 +1723,56 @@ def iterate_components(
             if not (skip_empty and self.static(c_name).empty)
         )
 
+    def rename_component_names(
+        self, component: str | Components, **kwargs: str
+    ) -> None:
+        """
+        Rename component names.
+
+        Rename components of component type and also update all cross-references of
+        the component in network.
+
+        Parameters
+        ----------
+        component : str or pypsa.Components
+            Component type or instance of pypsa.Components.
+        **kwargs
+            Mapping of old names to new names.
+
+        Returns
+        -------
+        None
+
+        Examples
+        --------
+        Define some network
+        >>> import pypsa
+        >>> n = pypsa.Network()
+        >>> n.add("Bus", ["bus1"])
+        Index(['bus1'], dtype='object')
+        >>> n.add("Generator", ["gen1"], bus="bus1")
+        Index(['gen1'], dtype='object')
+
+        Now rename the bus component
+
+        >>> n.rename_component_names("Bus", bus1="bus2")
+
+        Which updates the bus components
+
+        >>> n.buses.index
+        Index(['bus2'], dtype='object', name='Bus')
+
+        and all references in the network
+
+        >>> n.generators.bus
+        Generator
+        gen1    bus2
+        Name: bus, dtype: object
+
+        """
+        c = as_components(self, component)
+        c.rename_component_names(**kwargs)
+
 
 class SubNetwork:
     """


Test patch:
diff --git a/test/test_components_abstract.py b/test/test_components_abstract.py
deleted file mode 100644
index 0b808e56c..000000000
--- a/test/test_components_abstract.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import pytest
-
-import pypsa
-
-component_names = [
-    "Generator",
-    "Line",
-    "Link",
-    "StorageUnit",
-    "Load",
-    "Transformer",
-]
-
-
-@pytest.mark.parametrize("component_name", component_names)
-def test_alias_properties(ac_dc_network, component_name):
-    n = ac_dc_network
-    ct = pypsa.components.types.get(component_name)
-    c = n.components[component_name]
-
-    assert c.name == ct.name
-    assert c.list_name == ct.list_name
-    assert c.description == ct.description
-    assert c.category == ct.category
-    assert c.type == ct.category
-    assert c.defaults.equals(ct.defaults)
-    assert c.attrs.equals(ct.defaults)
-    if c.standard_types is not None:
-        assert c.standard_types.equals(ct.standard_types)
-
-    assert c.df is c.static
-    assert c.pnl is c.dynamic
-
-
-def test_network_attachments(ac_dc_network):
-    n = ac_dc_network.copy()
-
-    c = n.components.generators
-
-    assert c.n is n
-    assert c.n_save is n
-    assert c.attached is True
-    c.n = None
-    c.attached is False
-    with pytest.raises(AttributeError):
-        c.n_save
-
-
-def test_components_repr(ac_dc_network):
-    n = ac_dc_network
-    c = n.components.generators
-
-    assert repr(c).startswith("PyPSA 'Generator' Components")
diff --git a/test/test_network.py b/test/test_network.py
index 5e9409cd7..3d366b651 100644
--- a/test/test_network.py
+++ b/test/test_network.py
@@ -446,3 +446,49 @@ def test_components_referencing(ac_dc_network):
     assert id(ac_dc_network.buses) == id(ac_dc_network.components.buses.static)
     assert id(ac_dc_network.buses_t) == id(ac_dc_network.components.buses.dynamic)
     assert id(ac_dc_network.components.buses) == id(ac_dc_network.components.Bus)
+
+
+@pytest.mark.parametrize("use_component", [True, False])
+def test_rename_component_names(use_component):
+    n = pypsa.Network()
+    n.snapshots = [0, 1]
+
+    n.add("Bus", "bus1", v_mag_pu_set=[0.1, 0.2])
+    n.add("Bus", "bus2", v_mag_pu_set=[0.1, 0.2])
+    n.add("Line", "line1", bus0="bus1", bus1="bus2", s_max_pu=[0.1, 0.2])
+    n.add("Generator", "gen1", bus="bus1", p_min_pu=[0.1, 0.2])
+
+    if use_component:
+        c = n.c.buses
+        c.rename_component_names(bus1="bus3")
+    else:
+        n.rename_component_names("Bus", bus1="bus3")
+
+    with pytest.raises(ValueError):
+        n.rename_component_names("Bus", bus1=10)
+
+    assert "bus1" not in n.c.buses.static.index
+    assert "bus1" not in n.c.buses.dynamic.v_mag_pu_set.columns
+    assert "bus2" in n.c.buses.static.index
+    assert "bus2" in n.c.buses.dynamic.v_mag_pu_set.columns
+    assert "bus3" in n.c.buses.static.index
+    assert "bus3" in n.c.buses.dynamic.v_mag_pu_set.columns
+
+    assert "bus1" not in n.c.lines.static.bus0.to_list()
+    assert "bus1" not in n.c.lines.static.bus1.to_list()
+    assert "bus3" in n.c.lines.static.bus0.to_list()
+    assert "bus2" in n.c.lines.static.bus1.to_list()
+
+    assert "bus1" not in n.c.generators.static.bus.to_list()
+    assert "bus3" in n.c.generators.static.bus.to_list()
+
+
+def test_components_repr(ac_dc_network):
+    n = ac_dc_network
+
+    assert repr(n).startswith("PyPSA Network 'AC-DC'")
+    assert len(repr(n)) > len(str(n))
+
+    n = pypsa.Network()
+    assert repr(n).startswith("Empty Unnamed PyPSA Network")
+    assert len(repr(n)) > len(str(n))