Repository: cvxpy/cvxpy (language: cpp)

Issue:
Incorrect solution from quasilinear constraint
**Describe the bug**
cp.sum(cp.sign(x)) constraints not being enforced in a DQCP.

**To Reproduce**
x = cp.Variable(2)
obj = cp.sum_squares(np.ones(2) - x)
constr = [cp.sum(cp.sign(x)) <= 1]
prob = cp.Problem(cp.Minimize(obj), constr)
prob.solve(qcp = True)

Also try this with <= 0 instead of 1 and it will raise a recursion error.

**Expected behavior**
The optimal solution of this problem should be (0, 1) but x.value returns (1, 1)


**Version**
 - OS: M1 Mac monterey
 - CVXPY Version: 1.2.0

complex2real dispatches wrong canonicalizer for ``Zero`` cone constraints.
Consider the following trivial test case:
```
import numpy as np
import cvxpy as cp
from cvxpy.constraints.zero import Equality, Zero

def make_problem(use_zero: bool):
   # min{ real(x) s.t. x \in C, x == 0, real(x) >= -1, imag(x) <= 1.0 }
    x = cp.Variable(complex=True)
    constraints = [cp.real(x) >= -1.0, cp.imag(x) <= 1.0]
    if use_zero:
        constraints.append(Zero(x))
    else:
        constraints.append(Equality(x, cp.Constant(np.array(0.0)))
    prob = cp.Problem(cp.Minimize(cp.real(x)), constraints)
    return prob
``` 
Running ``make_prob(True).solve(verbose=True)`` raises an error, while running ``make_prob(False).solve(verbose=True)`` succeeds. The issue is in these lines:
https://github.com/cvxpy/cvxpy/blob/cb2170114b267be366beccf6db97af264f042ee9/cvxpy/reductions/complex2real/atom_canonicalizers/__init__.py#L96-L97
Specifically, notice that ``Zero`` maps to ``equality_canon`` when it should map to ``zero_canon``.




Discussion / hints:
Your problem should not be ``qcp``. It's a bug in the problem validation. QCP is only supposed to handle ``sign`` applied to scalars. @akshayka can you confirm?


Fix patch:
diff --git a/continuous_integration/install_dependencies.sh b/continuous_integration/install_dependencies.sh
index dd48a155d6..e38e93696a 100644
--- a/continuous_integration/install_dependencies.sh
+++ b/continuous_integration/install_dependencies.sh
@@ -43,8 +43,11 @@ fi
 
 if [[ "$PYTHON_VERSION" == "3.10" ]]; then
   python -m pip install diffcp gurobipy
+# Python 3.8 on Windows will uninstall NumPy 1.16 and install NumPy 1.24 without the exception.
+elif [[ "$RUNNER_OS" == "Windows" ]] && [[ "$PYTHON_VERSION" == "3.8" ]]; then
+  :
 elif [[ "$PYTHON_VERSION" == "3.6" ]]; then
-  python -m pip install diffcp xpress
+  python -m pip install xpress
 else
   python -m pip install diffcp gurobipy xpress
 fi
diff --git a/cvxpy/atoms/elementwise/elementwise.py b/cvxpy/atoms/elementwise/elementwise.py
index f02164efbe..d59dcc2893 100644
--- a/cvxpy/atoms/elementwise/elementwise.py
+++ b/cvxpy/atoms/elementwise/elementwise.py
@@ -60,7 +60,7 @@ def elemwise_grad_to_diag(value, rows, cols):
         """
         if not np.isscalar(value):
             value = value.ravel(order='F')
-        return sp.dia_matrix((value, [0]), shape=(rows, cols)).tocsc()
+        return sp.dia_matrix((np.atleast_1d(value), [0]), shape=(rows, cols)).tocsc()
 
     @staticmethod
     def _promote(arg, shape: Tuple[int, ...]):
diff --git a/cvxpy/atoms/log_det.py b/cvxpy/atoms/log_det.py
index 27ff61c638..cc9cabb5b4 100644
--- a/cvxpy/atoms/log_det.py
+++ b/cvxpy/atoms/log_det.py
@@ -39,10 +39,10 @@ def numeric(self, values):
         For PSD matrix A, this is the sum of logs of eigenvalues of A
         and is equivalent to the nuclear norm of the matrix logarithm of A.
         """
-        # take symmetric part of the input.
-        symm = (values[0] + values[0].T)/2
+        # take Hermitian part of the input.
+        symm = (values[0] + np.conj(values[0].T))/2
         sign, logdet = LA.slogdet(symm)
-        if sign == 1:
+        if np.isclose(np.real(sign), 1):
             return logdet
         else:
             return -np.inf
diff --git a/cvxpy/constraints/exponential.py b/cvxpy/constraints/exponential.py
index 6eb6dfec9f..4c12bb4129 100644
--- a/cvxpy/constraints/exponential.py
+++ b/cvxpy/constraints/exponential.py
@@ -58,13 +58,16 @@ def __init__(self, x, y, z, constr_id=None) -> None:
         self.x = Expression.cast_to_const(x)
         self.y = Expression.cast_to_const(y)
         self.z = Expression.cast_to_const(z)
+        args = [self.x, self.y, self.z]
+        for val in args:
+            if not (val.is_affine() and val.is_real()):
+                raise ValueError('All arguments must be affine and real.')
         xs, ys, zs = self.x.shape, self.y.shape, self.z.shape
         if xs != ys or xs != zs:
             msg = ("All arguments must have the same shapes. Provided arguments have"
                    "shapes %s" % str((xs, ys, zs)))
             raise ValueError(msg)
-        super(ExpCone, self).__init__([self.x, self.y, self.z],
-                                      constr_id)
+        super(ExpCone, self).__init__(args, constr_id)
 
     def __str__(self) -> str:
         return "ExpCone(%s, %s, %s)" % (self.x, self.y, self.z)
diff --git a/cvxpy/constraints/nonpos.py b/cvxpy/constraints/nonpos.py
index eac7e483b3..2af1556137 100644
--- a/cvxpy/constraints/nonpos.py
+++ b/cvxpy/constraints/nonpos.py
@@ -41,6 +41,8 @@ class NonPos(Constraint):
     """
     def __init__(self, expr, constr_id=None) -> None:
         super(NonPos, self).__init__([expr], constr_id)
+        if not self.args[0].is_real():
+            raise ValueError("Input to NonPos must be real.")
 
     def name(self) -> str:
         return "%s <= 0" % self.args[0]
@@ -100,6 +102,8 @@ class NonNeg(Constraint):
     """
     def __init__(self, expr, constr_id=None) -> None:
         super(NonNeg, self).__init__([expr], constr_id)
+        if not self.args[0].is_real():
+            raise ValueError("Input to NonNeg must be real.")
 
     def name(self) -> str:
         return "0 <= %s" % self.args[0]
diff --git a/cvxpy/constraints/power.py b/cvxpy/constraints/power.py
index a50ed50e9a..01a906908e 100644
--- a/cvxpy/constraints/power.py
+++ b/cvxpy/constraints/power.py
@@ -81,7 +81,7 @@ def residual(self):
         return problem.solve(solver='SCS', eps=1e-8)
 
     def get_data(self):
-        return [self.alpha]
+        return [self.alpha, self.id]
 
     def is_imag(self) -> bool:
         return False
@@ -202,7 +202,7 @@ def is_complex(self) -> bool:
         return False
 
     def get_data(self):
-        return [self.alpha, self.axis]
+        return [self.alpha, self.axis, self.id]
 
     @property
     def residual(self):
diff --git a/cvxpy/constraints/second_order.py b/cvxpy/constraints/second_order.py
index 395e107f03..d8dea150a7 100644
--- a/cvxpy/constraints/second_order.py
+++ b/cvxpy/constraints/second_order.py
@@ -117,7 +117,7 @@ def get_data(self):
         -------
         list
         """
-        return [self.axis]
+        return [self.axis, self.id]
 
     def num_cones(self):
         """The number of elementwise cones.
diff --git a/cvxpy/reductions/complex2real/complex2real.py b/cvxpy/reductions/complex2real/complex2real.py
index 6593992b08..782d56cc0d 100644
--- a/cvxpy/reductions/complex2real/complex2real.py
+++ b/cvxpy/reductions/complex2real/complex2real.py
@@ -18,6 +18,7 @@
 from cvxpy import settings as s
 from cvxpy.constraints import (PSD, SOC, Equality, Inequality, NonNeg, NonPos,
                                Zero,)
+from cvxpy.constraints.constraint import Constraint
 from cvxpy.expressions import cvxtypes
 from cvxpy.lin_ops import lin_utils as lu
 from cvxpy.reductions import InverseData, Solution
@@ -34,6 +35,8 @@ def accepts(problem) -> bool:
 class Complex2Real(Reduction):
     """Lifts complex numbers to a real representation."""
 
+    UNIMPLEMENTED_COMPLEX_DUALS = (SOC,)
+
     def accepts(self, problem) -> None:
         accepts(problem)
 
@@ -57,10 +60,14 @@ def apply(self, problem):
             # created for the imaginary part.
             real_constrs, imag_constrs = self.canonicalize_tree(
                 constraint, inverse_data.real2imag, leaf_map)
-            if real_constrs is not None:
+            if isinstance(real_constrs, list):
                 constrs.extend(real_constrs)
-            if imag_constrs is not None:
+            elif isinstance(real_constrs, Constraint):
+                constrs.append(real_constrs)
+            if isinstance(imag_constrs, list):
                 constrs.extend(imag_constrs)
+            elif isinstance(imag_constrs, Constraint):
+                constrs.append(imag_constrs)
 
         new_problem = problems.problem.Problem(real_obj,
                                                constrs)
@@ -101,24 +108,26 @@ def invert(self, solution, inverse_data):
                         imag_id = inverse_data.real2imag[cid]
                         dvars[cid] = 1j*solution.dual_vars[imag_id]
                     # For equality and inequality constraints.
-                    elif isinstance(cons,
-                                    (Equality, Zero, Inequality,
-                                     NonNeg, NonPos)
-                                    ) and cons.is_complex():
+                    # All cases that follow are for complex-valued constraints:
+                    #   1. check inequality / equality constraints.
+                    #   2. check PSD constraints.
+                    #   3. check if a constraint is known to lack a complex dual implementation
+                    #   4. raise an error
+                    elif isinstance(cons, (Equality, Zero, Inequality, NonNeg, NonPos)):
                         imag_id = inverse_data.real2imag[cid]
                         if imag_id in solution.dual_vars:
                             dvars[cid] = solution.dual_vars[cid] + \
                                 1j*solution.dual_vars[imag_id]
                         else:
                             dvars[cid] = solution.dual_vars[cid]
-                    elif isinstance(cons, SOC) and cons.is_complex():
-                        # TODO add dual variables for complex SOC.
-                        pass
                     # For PSD constraints.
-                    elif isinstance(cons, PSD) and cons.is_complex():
+                    elif isinstance(cons, PSD):
                         n = cons.args[0].shape[0]
                         dual = solution.dual_vars[cid]
                         dvars[cid] = dual[:n, :n] + 1j*dual[n:, :n]
+                    elif isinstance(cons, self.UNIMPLEMENTED_COMPLEX_DUALS):
+                        # TODO: implement dual variable recovery
+                        pass
                     else:
                         raise Exception("Unknown constraint type.")
 
@@ -152,4 +161,5 @@ def canonicalize_expr(self, expr, real_args, imag_args, real2imag, leaf_map):
             return result
         else:
             assert all(v is None for v in imag_args)
-            return expr.copy(real_args), None
+            real_out = expr.copy(real_args)
+            return real_out, None
diff --git a/cvxpy/reductions/cone2cone/exotic2common.py b/cvxpy/reductions/cone2cone/exotic2common.py
index b1f5a1caf1..310bc35c62 100644
--- a/cvxpy/reductions/cone2cone/exotic2common.py
+++ b/cvxpy/reductions/cone2cone/exotic2common.py
@@ -41,7 +41,7 @@ def pow_nd_canon(con, args):
     args : tuple of length two
         W,z = args[0], args[1]
     """
-    alpha, axis = con.get_data()
+    alpha, axis, _ = con.get_data()
     alpha = alpha.value
     W, z = args
     if axis == 1:
diff --git a/cvxpy/reductions/solvers/conic_solvers/gurobi_conif.py b/cvxpy/reductions/solvers/conic_solvers/gurobi_conif.py
index 7fb653e5e9..ee50aff3fc 100644
--- a/cvxpy/reductions/solvers/conic_solvers/gurobi_conif.py
+++ b/cvxpy/reductions/solvers/conic_solvers/gurobi_conif.py
@@ -182,7 +182,16 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
 
         leq_start = dims[s.EQ_DIM]
         leq_end = dims[s.EQ_DIM] + dims[s.LEQ_DIM]
-        if hasattr(model, 'addMConstrs'):
+        if hasattr(model, 'addMConstr'):
+            # Code path for Gurobi v10.0-
+            eq_constrs = model.addMConstr(
+                A[:leq_start, :], None, gurobipy.GRB.EQUAL, b[:leq_start]
+            ).tolist()
+            ineq_constrs = model.addMConstr(
+                A[leq_start:leq_end, :], None, gurobipy.GRB.LESS_EQUAL,
+                b[leq_start:leq_end]).tolist()
+        elif hasattr(model, 'addMConstrs'):
+            # Code path for Gurobi v9.0-v9.5
             eq_constrs = model.addMConstrs(
                 A[:leq_start, :], None, gurobipy.GRB.EQUAL, b[:leq_start])
             ineq_constrs = model.addMConstrs(
diff --git a/cvxpy/reductions/solvers/qp_solvers/gurobi_qpif.py b/cvxpy/reductions/solvers/qp_solvers/gurobi_qpif.py
index d20b39b95a..c769cab48a 100644
--- a/cvxpy/reductions/solvers/qp_solvers/gurobi_qpif.py
+++ b/cvxpy/reductions/solvers/qp_solvers/gurobi_qpif.py
@@ -157,16 +157,14 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
         x = np.array(model.getVars(), copy=False)
 
         if A.shape[0] > 0:
-            if hasattr(model, 'addMConstrs'):
+            if hasattr(model, 'addMConstr'):
                 # We can pass all of A @ x == b at once, use stable API
-                # introduced with Gurobi v9
+                # introduced with Gurobi v9.5
+                model.addMConstr(A, None, grb.GRB.EQUAL, b)
+            elif hasattr(model, 'addMConstrs'):
+                # We can pass all of A @ x == b at once, use (now) deprecated
+                # API introduced with Gurobi v9.0
                 model.addMConstrs(A, None, grb.GRB.EQUAL, b)
-            elif hasattr(model, '_v811_addMConstrs'):
-                # We can pass all of A @ x == b at once, API only for Gurobi
-                # v811
-                A.eliminate_zeros()  # Work around bug in gurobipy v811
-                sense = np.repeat(grb.GRB.EQUAL, A.shape[0])
-                model._v811_addMConstrs(A, sense, b)
             else:
                 # Add equality constraints: iterate over the rows of A
                 # adding each row into the model
@@ -180,16 +178,14 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
         model.update()
 
         if F.shape[0] > 0:
-            if hasattr(model, 'addMConstrs'):
-                # We can pass all of F @ x <= g at once, use stable API
-                # introduced with Gurobi v9
+            if hasattr(model, 'addMConstr'):
+                # We can pass all of A @ x == b at once, use stable API
+                # introduced with Gurobi v9.5
+                model.addMConstr(F, None, grb.GRB.LESS_EQUAL, g)
+            elif hasattr(model, 'addMConstrs'):
+                # We can pass all of A @ x == b at once, use (now) deprecated
+                # API introduced with Gurobi v9.0
                 model.addMConstrs(F, None, grb.GRB.LESS_EQUAL, g)
-            elif hasattr(model, '_v811_addMConstrs'):
-                # We can pass all of F @ x <= g at once, API only for Gurobi
-                # v811.
-                F.eliminate_zeros()  # Work around bug in gurobipy v811
-                sense = np.repeat(grb.GRB.LESS_EQUAL, F.shape[0])
-                model._v811_addMConstrs(F, sense, g)
             else:
                 # Add inequality constraints: iterate over the rows of F
                 # adding each row into the model
diff --git a/cvxpy/utilities/canonical.py b/cvxpy/utilities/canonical.py
index db2c6e141c..a331f6523d 100644
--- a/cvxpy/utilities/canonical.py
+++ b/cvxpy/utilities/canonical.py
@@ -92,6 +92,8 @@ def copy(self, args=None, id_objects=None):
             return id_objects[id(self)]
         if args is None:
             args = self.args
+        else:
+            assert len(args) == len(self.args)
         data = self.get_data()
         if data is not None:
             return type(self)(*(args + data))
diff --git a/pyproject.toml b/pyproject.toml
index 746d00f961..57626d0c52 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,6 +22,6 @@ requires = [
     "numpy>=1.21,<1.22; python_version=='3.10' and (platform_machine!='arm64' or platform_system!='Darwin')",
     "numpy>=1.21.4,<1.22; python_version=='3.10' and (platform_machine=='arm64' and platform_system=='Darwin')",
     "scipy >= 1.1.0",
-    "setuptools>=40.8.0",
+    "setuptools>=40.8.0,<=64.0.2",
     "wheel"
 ]
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 95646f8115..ee1c13090c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,25 @@
+[options.extras_require]
+# Solver names as in cvxpy.settings = pip-installable distribution providing it
+CBC = cylp>=0.91.5
+CLARABEL = clarabel
+CVXOPT = cvxopt
+DIFFCP = diffcp
+ECOS =
+ECOS_BB =
+GLOP = ortools>=9.3,<9.5
+GLPK = cvxopt
+GLPK_MI = cvxopt
+GUROBI = gurobipy
+HIGHS = scipy>=1.6.1
+MOSEK = Mosek
+OSQP =
+PDLP = ortools>=9.3,<9.5
+PROXQP = proxsuite
+SCIP = PySCIPOpt
+SCIPY = scipy
+SCS = setuptools<64.0.2
+XPRESS = xpress
+
 [flake8]
 max-line-length=100
 exclude=build,examples,doc,cvxpy/cvxcore/*,*__init__.py
diff --git a/setup.py b/setup.py
index 7efa8c59fe..addbd731ca 100644
--- a/setup.py
+++ b/setup.py
@@ -231,7 +231,8 @@ def not_on_windows(s: str) -> str:
         "ecos >= 2",
         "scs >= 1.1.6",
         "numpy >= 1.15",
-        "scipy >= 1.1.0"
+        "scipy >= 1.1.0",
+        "setuptools <= 64.0.2",
     ],
     setup_requires=["numpy >= 1.15"],
 )


Test patch:
diff --git a/cvxpy/tests/test_complex.py b/cvxpy/tests/test_complex.py
index 5c0ec96b1a..726a444c48 100644
--- a/cvxpy/tests/test_complex.py
+++ b/cvxpy/tests/test_complex.py
@@ -18,6 +18,7 @@
 import scipy.sparse as sp
 
 import cvxpy as cp
+import cvxpy.constraints as constraints
 from cvxpy import Minimize, Problem
 from cvxpy.expressions.constants import Constant, Parameter
 from cvxpy.expressions.variable import Variable
@@ -314,11 +315,25 @@ def test_log_det(self) -> None:
         P = np.arange(9) - 2j*np.arange(9)
         P = np.reshape(P, (3, 3))
         P = np.conj(P.T).dot(P)/100 + np.eye(3)*.1
-        value = cp.log_det(P).value
+        logdet_value = cp.log_det(P).value
         X = Variable((3, 3), complex=True)
-        prob = Problem(cp.Maximize(cp.log_det(X)), [X == P])
+        objective = cp.log_det(X)
+        prob = Problem(cp.Maximize(objective), [X == P])
         result = prob.solve(solver=cp.SCS, eps=1e-6)
-        self.assertAlmostEqual(result, value, places=2)
+        self.assertAlmostEqual(result, logdet_value, places=2)
+        objective_value = objective.value
+        _, ld = np.linalg.slogdet(P)
+        self.assertAlmostEqual(objective_value, ld, places=3)
+
+        # Test case for Issue 1816.
+        #   The optimal solution is the identity matrix scaled by 3.
+        #   NumPy's slogdet function returns a sign "s" with a tiny complex
+        #   part which causes 1 == s to fail.
+        cons = [X >> 0, cp.real(cp.trace(X)) <= 9]
+        obj = cp.Maximize(cp.log_det(X))
+        prob = cp.Problem(objective=obj, constraints=cons)
+        prob.solve(solver=cp.SCS, eps=1e-6)
+        self.assertAlmostEqual(obj.value, 3*np.log(3))
 
     def test_eigval_atoms(self) -> None:
         """Test eigenvalue atoms.
@@ -619,3 +634,71 @@ def test_bool(self) -> None:
         prob = cp.Problem(obj, constraints)
         prob.solve(solver='ECOS_BB')
         self.assertAlmostEqual(prob.value, 1, places=4)
+
+    def test_duals(self) -> None:
+        np.random.seed(0)
+        u_real = np.random.rand(3)
+        u_imag = np.random.rand(3)
+        u = u_real + 1j * u_imag
+
+        y = cp.Variable(shape=(3,))
+        helper_objective = cp.Minimize(cp.norm(y - u_real))
+
+        ####################################
+        #
+        #   Compute reference values
+        #
+        ####################################
+        con_a = constraints.PowCone3D(y[0], y[1], y[2], [0.25])
+        helper_prob_a = cp.Problem(helper_objective, [con_a])
+        helper_prob_a.solve()
+        expect_dual_a = con_a.dual_value
+
+        con_b = constraints.ExpCone(y[0], y[1], y[2])
+        helper_prob_b = cp.Problem(helper_objective, [con_b])
+        helper_prob_b.solve()
+        expect_dual_b = con_b.dual_value
+
+        con_c = cp.SOC(y[2], y[:2])
+        helper_prob_c = cp.Problem(helper_objective, [con_c])
+        helper_prob_c.solve()
+        expect_dual_c = con_c.dual_value
+
+        ####################################
+        #
+        #   Run tests
+        #
+        ####################################
+        x = cp.Variable(shape=(3,), complex=True)
+        actual_objective = cp.Minimize(cp.norm(x - u))
+        coupling_con = cp.real(x) == y
+
+        con_a_test = con_a.copy()
+        prob_a = cp.Problem(actual_objective, [coupling_con, con_a_test])
+        prob_a.solve()
+        actual_dual_a = con_a_test.dual_value
+        self.assertItemsAlmostEqual(actual_dual_a, expect_dual_a, places=2)
+
+        con_b_test = con_b.copy()
+        prob_b = cp.Problem(actual_objective, [coupling_con, con_b_test])
+        prob_b.solve()
+        actual_dual_b = con_b_test.dual_value
+        self.assertItemsAlmostEqual(actual_dual_b, expect_dual_b, places=2)
+
+        con_c_test = con_c.copy()
+        prob_c = cp.Problem(actual_objective, [coupling_con, con_c_test])
+        prob_c.solve()
+        actual_dual_c = con_c_test.dual_value
+        self.assertItemsAlmostEqual(actual_dual_c[0], expect_dual_c[0], places=2)
+        self.assertItemsAlmostEqual(actual_dual_c[1], expect_dual_c[1], places=2)
+
+    def test_illegal_complex_args(self) -> None:
+        x = cp.Variable(shape=(3,), complex=True)
+        with self.assertRaises(ValueError):
+            constraints.ExpCone(x[0], x[1], x[2])
+        with self.assertRaises(ValueError):
+            constraints.PowCone3D(x[0], x[1], x[2], [0.5])
+        with self.assertRaises(ValueError):
+            constraints.NonNeg(x)
+        with self.assertRaises(ValueError):
+            cp.NonPos(x)
diff --git a/cvxpy/tests/test_constraints.py b/cvxpy/tests/test_constraints.py
index e597ec7943..7516afb5db 100644
--- a/cvxpy/tests/test_constraints.py
+++ b/cvxpy/tests/test_constraints.py
@@ -78,7 +78,7 @@ def test_equality(self) -> None:
         self.assertEqual(copy.args, constr.args)
         self.assertFalse(copy.args is constr.args)
         # Test copy with new args
-        copy = constr.copy(args=[self.A])
+        copy = constr.copy(args=[self.A, self.B])
         self.assertTrue(type(copy) is type(constr))
         self.assertTrue(copy.args[0] is self.A)
 
@@ -122,7 +122,7 @@ def test_inequality(self) -> None:
         self.assertEqual(copy.args, constr.args)
         self.assertFalse(copy.args is constr.args)
         # Test copy with new args
-        copy = constr.copy(args=[self.A])
+        copy = constr.copy(args=[self.A, self.B])
         self.assertTrue(type(copy) is type(constr))
         self.assertTrue(copy.args[0] is self.A)