///|
/// Checks equality between two Python objects using the `__eq__` method
/// of the first object.
pub fn[A : PyObjectInto, B : PyObjectInto] py_equal(
  sel : A,
  other : B,
) -> Bool raise @py.PyRuntimeError {
  let a = sel.to_py_object()
  let b = other.to_py_object()
  guard a.get_attr("__eq__") is Some(PyCallable(mtd)) else { false }
  let args = @py.PyTuple::new(1)
  args..set(0, b)
  guard mtd.invoke(args~) is Some(PyBool(r)) else { false }
  @py.PyBool::to_bool(r)
}

///|
test "eq int" {
  assert_true(py_equal(42, 42))
  assert_false(py_equal(42, 432))
  assert_false(py_equal(42, "42"))
  assert_true(py_equal("hello", "hello"))
}

///|
pub fn[A : PyObjectInto] invoke_unary(
  sel : A,
  mtd : String,
) -> @py.PyObjectEnum? raise @py.PyRuntimeError {
  let obj = sel.to_py_object()
  guard obj.get_attr(mtd) is Some(PyCallable(mtd)) else { None }
  let args = @py.PyTuple::new(0)
  mtd.invoke(args~)
}

///|
pub fn[A : PyObjectInto, B : PyObjectInto] invoke_binary(
  sel : A,
  other : B,
  mtd : String,
) -> @py.PyObjectEnum? raise @py.PyRuntimeError {
  let obj = sel.to_py_object()
  guard obj.get_attr(mtd) is Some(PyCallable(mtd)) else { None }
  let args = @py.PyTuple::new(1)
  args.set(0, other.to_py_object())
  mtd.invoke(args~)
}

///|
pub suberror CallError {
  MtdNotFound(String)
  UnexpectedReturnType(String)
  RuntimeError(@py.PyRuntimeError)
}

///|
pub fn[A : PyObjectInto] py_repr(sel : A) -> String raise CallError {
  match (invoke_unary(sel, "__repr__") catch { e => raise RuntimeError(e) }) {
    Some(PyString(repr)) => @py.PyString::to_string(repr)
    Some(en) => raise UnexpectedReturnType(en.to_string())
    None => raise MtdNotFound("__repr__")
  }
}

///|
pub fn[A : PyObjectInto] py_str(sel : A) -> String raise CallError {
  match (invoke_unary(sel, "__str__") catch { e => raise RuntimeError(e) }) {
    Some(PyString(s)) => @py.PyString::to_string(s)
    Some(en) => raise UnexpectedReturnType(en.to_string())
    None => raise MtdNotFound("__str__")
  }
}

///|
test "py_repr" {
  assert_eq(py_repr(42), "42")
  assert_eq(py_repr("hello"), "'hello'")
  assert_eq(py_repr([1, 2, 3]), "[1, 2, 3]")
  assert_eq(py_repr(TypedPyTuple2::new(1, 2)), "(1, 2)")
  assert_eq(py_repr(TypedPyTuple2::new(1, "2")), "(1, '2')")
  assert_eq(py_str("hello"), "hello")
  assert_eq(py_str(3.14), "3.14")
  assert_eq(py_str(1.0 / 2), "0.5")
  inspect(invoke_binary(12, 42, "__add__"), content="Some(PyInteger(54))")
}